From 25e657fa51ff7b55a9b3646d2975c0612162c075 Mon Sep 17 00:00:00 2001 From: Jim Date: Wed, 28 Apr 2021 06:53:13 -0400 Subject: [PATCH] Ongoing OIDC: support to request additional OIDC scopes from the IdP (#1175) --- .../oidc_auth_method_attributes.gen.go | 1 + api/authmethods/option.gen.go | 24 ++ internal/auth/oidc/account_test.go | 2 +- internal/auth/oidc/auth_method.go | 40 ++- internal/auth/oidc/auth_method_test.go | 26 +- internal/auth/oidc/claims_scope.go | 79 ++++++ internal/auth/oidc/claims_scope_test.go | 265 +++++++++++++++++ internal/auth/oidc/immutable_fields_test.go | 2 +- internal/auth/oidc/options.go | 8 + internal/auth/oidc/options_test.go | 8 + internal/auth/oidc/repository_account.go | 8 +- internal/auth/oidc/repository_account_test.go | 36 +-- internal/auth/oidc/repository_auth_method.go | 4 +- .../oidc/repository_auth_method_create.go | 7 + .../repository_auth_method_create_test.go | 3 + ...epository_auth_method_operational_state.go | 2 +- .../auth/oidc/repository_auth_method_read.go | 4 + .../oidc/repository_auth_method_read_test.go | 9 +- .../oidc/repository_auth_method_update.go | 148 +++++++--- .../repository_auth_method_update_test.go | 190 +++++++++---- internal/auth/oidc/service_start_auth.go | 4 + internal/auth/oidc/service_start_auth_test.go | 11 +- internal/auth/oidc/service_test.go | 7 +- .../auth/oidc/service_token_request_test.go | 8 +- internal/auth/oidc/store/oidc.pb.go | 267 +++++++++++++----- internal/auth/oidc/testing.go | 27 +- .../cmd/commands/authmethodscmd/oidc_funcs.go | 16 ++ .../migrations/postgres/5/01_oidc.up.sql | 85 ++++++ internal/db/schema/postgres_migration.gen.go | 84 +++++- .../resources/authmethods/auth_method.pb.go | 112 ++++---- .../authmethods/v1/auth_method.proto | 215 +++++++------- .../storage/auth/oidc/store/v1/oidc.proto | 23 ++ .../authmethods/authmethod_service.go | 17 ++ .../authmethods/authmethod_service_test.go | 26 ++ .../controller/handlers/authmethods/oidc.go | 5 + .../handlers/authmethods/oidc_test.go | 39 +++ 36 files changed, 1432 insertions(+), 380 deletions(-) create mode 100644 internal/auth/oidc/claims_scope.go create mode 100644 internal/auth/oidc/claims_scope_test.go create mode 100644 internal/db/schema/migrations/postgres/5/01_oidc.up.sql diff --git a/api/authmethods/oidc_auth_method_attributes.gen.go b/api/authmethods/oidc_auth_method_attributes.gen.go index fd25f1edd..dded7bf84 100644 --- a/api/authmethods/oidc_auth_method_attributes.gen.go +++ b/api/authmethods/oidc_auth_method_attributes.gen.go @@ -13,6 +13,7 @@ type OidcAuthMethodAttributes struct { CallbackUrl string `json:"callback_url,omitempty"` IdpCaCerts []string `json:"idp_ca_certs,omitempty"` AllowedAudiences []string `json:"allowed_audiences,omitempty"` + ClaimsScopes []string `json:"claims_scopes,omitempty"` DisableDiscoveredConfigValidation bool `json:"disable_discovered_config_validation,omitempty"` DryRun bool `json:"dry_run,omitempty"` } diff --git a/api/authmethods/option.gen.go b/api/authmethods/option.gen.go index 7e53e3b44..23246c400 100644 --- a/api/authmethods/option.gen.go +++ b/api/authmethods/option.gen.go @@ -147,6 +147,30 @@ func DefaultAttributes() Option { } } +func WithOidcAuthMethodClaimsScopes(inClaimsScopes []string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["claims_scopes"] = inClaimsScopes + o.postMap["attributes"] = val + } +} + +func DefaultOidcAuthMethodClaimsScopes() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["claims_scopes"] = nil + o.postMap["attributes"] = val + } +} + func WithOidcAuthMethodClientId(inClientId string) Option { return func(o *options) { raw, ok := o.postMap["attributes"] diff --git a/internal/auth/oidc/account_test.go b/internal/auth/oidc/account_test.go index 9ac92fff5..848944496 100644 --- a/internal/auth/oidc/account_test.go +++ b/internal/auth/oidc/account_test.go @@ -206,7 +206,7 @@ func TestAccount_Create(t *testing.T) { discoUrl := am.GetIssuer() newDiscoUrl := "https://changed.com" am.Issuer = newDiscoUrl - n, err := rw.Update(ctx, am, []string{"Issuer"}, nil) + n, err := rw.Update(ctx, am, []string{IssuerField}, nil) require.NoError(t, err) assert.Equal(t, 1, n) assert.Equal(t, newDiscoUrl, am.GetIssuer()) diff --git a/internal/auth/oidc/auth_method.go b/internal/auth/oidc/auth_method.go index f7f51b0fa..2a035e1b4 100644 --- a/internal/auth/oidc/auth_method.go +++ b/internal/auth/oidc/auth_method.go @@ -82,6 +82,7 @@ func NewAuthMethod(scopeId string, clientId string, clientSecret ClientSecret, o ClientId: clientId, ClientSecret: string(clientSecret), MaxAge: int32(opts.withMaxAge), + ClaimsScopes: opts.withClaimsScopes, }, } if opts.withApiUrl != nil { @@ -106,7 +107,6 @@ func NewAuthMethod(scopeId string, clientId string, clientSecret ClientSecret, o a.SigningAlgs = append(a.SigningAlgs, string(alg)) } } - if a.OperationalState != string(InactiveState) { if err := a.isComplete(); err != nil { return nil, errors.Wrap(err, op, errors.WithMsg("new auth method being created with incomplete data but non-inactive state")) @@ -270,9 +270,10 @@ func (am *AuthMethod) isComplete() error { } type convertedValues struct { - Algs []interface{} - Auds []interface{} - Certs []interface{} + Algs []interface{} + Auds []interface{} + Certs []interface{} + ClaimsScopes []interface{} } // convertValueObjects converts the embedded value objects. It will return an @@ -283,7 +284,7 @@ func (am *AuthMethod) convertValueObjects() (*convertedValues, error) { return nil, errors.New(errors.InvalidPublicId, op, "missing public id") } var err error - var addAlgs, addAuds, addCerts []interface{} + var addAlgs, addAuds, addCerts, addScopes []interface{} if addAlgs, err = am.convertSigningAlgs(); err != nil { return nil, errors.Wrap(err, op) } @@ -293,10 +294,14 @@ func (am *AuthMethod) convertValueObjects() (*convertedValues, error) { if addCerts, err = am.convertCertificates(); err != nil { return nil, errors.Wrap(err, op) } + if addScopes, err = am.convertClaimsScopes(); err != nil { + return nil, errors.Wrap(err, op) + } return &convertedValues{ - Algs: addAlgs, - Auds: addAuds, - Certs: addCerts, + Algs: addAlgs, + Auds: addAuds, + Certs: addCerts, + ClaimsScopes: addScopes, }, nil } @@ -356,3 +361,22 @@ func (am *AuthMethod) convertCertificates() ([]interface{}, error) { } return newInterfaces, nil } + +// convertClaimsScopes converts the embedded claims scopes from []string +// to []interface{} where each slice element is a *ClaimsScope. It will return an +// error if the AuthMethod's public id is not set. +func (am *AuthMethod) convertClaimsScopes() ([]interface{}, error) { + const op = "oidc.(AuthMethod).convertClaimsScopes" + if am.PublicId == "" { + return nil, errors.New(errors.InvalidPublicId, op, "missing public id") + } + newInterfaces := make([]interface{}, 0, len(am.ClaimsScopes)) + for _, cs := range am.ClaimsScopes { + obj, err := NewClaimsScope(am.PublicId, cs) + if err != nil { + return nil, errors.Wrap(err, op) + } + newInterfaces = append(newInterfaces, obj) + } + return newInterfaces, nil +} diff --git a/internal/auth/oidc/auth_method_test.go b/internal/auth/oidc/auth_method_test.go index f8c6af3cb..b0903e5c7 100644 --- a/internal/auth/oidc/auth_method_test.go +++ b/internal/auth/oidc/auth_method_test.go @@ -565,12 +565,21 @@ func Test_convertValueObjects(t *testing.T) { require.NoError(t, err) testCertificates := []interface{}{c} + testScopes := []string{"profile", "email"} + testClaimsScopes := make([]interface{}, 0, len(testScopes)) + for _, s := range testScopes { + obj, err := NewClaimsScope(testPublicId, s) + require.NoError(t, err) + testClaimsScopes = append(testClaimsScopes, obj) + } + tests := []struct { name string authMethodId string algs []string auds []string certs []string + scopes []string wantValues *convertedValues wantErrMatch *errors.Template wantErrContains string @@ -581,10 +590,12 @@ func Test_convertValueObjects(t *testing.T) { algs: testAlgs, auds: testAuds, certs: testCerts, + scopes: testScopes, wantValues: &convertedValues{ - Algs: testSigningAlgs, - Auds: testAudiences, - Certs: testCertificates, + Algs: testSigningAlgs, + Auds: testAudiences, + Certs: testCertificates, + ClaimsScopes: testClaimsScopes, }, }, { @@ -602,6 +613,7 @@ func Test_convertValueObjects(t *testing.T) { SigningAlgs: tt.algs, AudClaims: tt.auds, Certificates: tt.certs, + ClaimsScopes: tt.scopes, }, } @@ -629,6 +641,14 @@ func Test_convertValueObjects(t *testing.T) { assert.Equal(tt.wantValues.Certs, convertedCerts) } + convertedScopes, err := am.convertClaimsScopes() + if tt.wantErrMatch != nil { + require.Error(err) + assert.Truef(errors.Match(tt.wantErrMatch, err), "wanted err %q and got: %+v", tt.wantErrMatch.Code, err) + } else { + assert.Equal(tt.wantValues.ClaimsScopes, convertedScopes) + } + values, err := am.convertValueObjects() if tt.wantErrMatch != nil { require.Error(err) diff --git a/internal/auth/oidc/claims_scope.go b/internal/auth/oidc/claims_scope.go new file mode 100644 index 000000000..5603e221d --- /dev/null +++ b/internal/auth/oidc/claims_scope.go @@ -0,0 +1,79 @@ +package oidc + +import ( + "github.com/hashicorp/boundary/internal/auth/oidc/store" + "github.com/hashicorp/boundary/internal/errors" + "google.golang.org/protobuf/proto" +) + +const ( + // defaultClaimsScopeTableName defines the default table name for an ClaimsScope + defaultClaimsScopeTableName = "auth_oidc_scope" + + DefaultClaimsScope = "openid" +) + +// ClaimsScope defines optional OIDC scope values that are used to request +// claims, in addition to the default scope of "openid" (see: DefaultClaimsScope). +// +// see: https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims +type ClaimsScope struct { + *store.ClaimsScope + tableName string +} + +func NewClaimsScope(authMethodId, claimsScope string) (*ClaimsScope, error) { + const op = "oidc.NewClaimsScope" + cs := &ClaimsScope{ + ClaimsScope: &store.ClaimsScope{ + OidcMethodId: authMethodId, + Scope: claimsScope, + }, + } + if err := cs.validate(op); err != nil { + return nil, err + } + return cs, nil +} + +// validate the ClaimsScope. On success, it will return nil. +func (cs *ClaimsScope) validate(caller errors.Op) error { + if cs.OidcMethodId == "" { + return errors.New(errors.InvalidParameter, caller, "missing oidc auth method id") + } + if cs.Scope == "" { + return errors.New(errors.InvalidParameter, caller, "missing claims scope") + } + if cs.Scope == DefaultClaimsScope { + return errors.New(errors.InvalidParameter, caller, "openid is the default scope and cannot be added as optional") + } + return nil +} + +// AllocClaimsScope makes an empty one in memory +func AllocClaimsScope() ClaimsScope { + return ClaimsScope{ + ClaimsScope: &store.ClaimsScope{}, + } +} + +// Clone a ClaimsScope +func (cs *ClaimsScope) Clone() *ClaimsScope { + cp := proto.Clone(cs.ClaimsScope) + return &ClaimsScope{ + ClaimsScope: cp.(*store.ClaimsScope), + } +} + +// TableName returns the table name. +func (s *ClaimsScope) TableName() string { + if s.tableName != "" { + return s.tableName + } + return defaultClaimsScopeTableName +} + +// SetTableName sets the table name. +func (s *ClaimsScope) SetTableName(n string) { + s.tableName = n +} diff --git a/internal/auth/oidc/claims_scope_test.go b/internal/auth/oidc/claims_scope_test.go new file mode 100644 index 000000000..d00232448 --- /dev/null +++ b/internal/auth/oidc/claims_scope_test.go @@ -0,0 +1,265 @@ +package oidc + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestClaimsScope_Create(t *testing.T) { + t.Parallel() + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + testAuthMethod := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "my-dogs-name", + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + + type args struct { + authMethodId string + claimsScope string + } + tests := []struct { + name string + args args + createResource bool + createWantErrMatch *errors.Template + want *ClaimsScope + wantErrMatch *errors.Template + }{ + { + name: "valid", + args: args{ + authMethodId: testAuthMethod.PublicId, + claimsScope: "profile", + }, + createResource: true, + want: func() *ClaimsScope { + want := AllocClaimsScope() + want.OidcMethodId = testAuthMethod.PublicId + want.Scope = "profile" + return &want + }(), + }, + { + name: "dup", + args: args{ + authMethodId: testAuthMethod.PublicId, + claimsScope: "profile", + }, + createResource: true, + want: func() *ClaimsScope { + want := AllocClaimsScope() + want.OidcMethodId = testAuthMethod.PublicId + want.Scope = "profile" + return &want + }(), + createWantErrMatch: errors.T(errors.NotUnique), + }, + { + name: "empty-auth-method", + args: args{ + authMethodId: "", + claimsScope: "empty-auth-method", + }, + wantErrMatch: errors.T(errors.InvalidParameter), + }, + { + name: "empty-aud", + args: args{ + authMethodId: testAuthMethod.PublicId, + claimsScope: "", + }, + wantErrMatch: errors.T(errors.InvalidParameter), + }, + { + name: "openid-default-error", + args: args{ + authMethodId: testAuthMethod.PublicId, + claimsScope: DefaultClaimsScope, + }, + wantErrMatch: errors.T(errors.InvalidParameter), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := NewClaimsScope(tt.args.authMethodId, tt.args.claimsScope) + if tt.wantErrMatch != nil { + require.Error(err) + assert.Nil(got) + assert.Truef(errors.Match(tt.wantErrMatch, err), "wanted error %s and got: %s", tt.wantErrMatch.Code, err.Error()) + return + } + require.NoError(err) + assert.Equal(tt.want, got) + if tt.createResource { + err := rw.Create(ctx, got) + if tt.createWantErrMatch != nil { + require.Error(err) + assert.Truef(errors.Match(tt.createWantErrMatch, err), "wanted error %s and got: %s", tt.createWantErrMatch.Code, err.Error()) + return + } + assert.NoError(err) + found := AllocClaimsScope() + require.NoError(rw.LookupWhere(ctx, &found, "oidc_method_id = ? and scope = ?", tt.args.authMethodId, tt.args.claimsScope)) + } + }) + } +} + +func TestClaimsScope_Delete(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + rw := db.New(conn) + + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + testAuthMethod := + TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "my-dogs-name", + WithApiUrl(TestConvertToUrls(t, "https://api.com")[0]), WithAudClaims("alice.com")) // seed an extra callback url to just make sure the delete only gets the right num of rows + + testResource := func(authMethodId string, claimsScope string) *ClaimsScope { + c, err := NewClaimsScope(authMethodId, claimsScope) + require.NoError(t, err) + return c + } + tests := []struct { + name string + claimsScope *ClaimsScope + wantRowsDeleted int + overrides func(*ClaimsScope) + wantErr bool + wantErrMsg string + }{ + { + name: "valid", + claimsScope: testResource(testAuthMethod.PublicId, "email"), + wantErr: false, + wantRowsDeleted: 1, + }, + { + name: "bad-OidcMethodId", + claimsScope: testResource(testAuthMethod.PublicId, "email"), + overrides: func(c *ClaimsScope) { c.OidcMethodId = "bad-id" }, + wantErr: false, + wantRowsDeleted: 0, + }, + { + name: "bad-scope", + claimsScope: testResource(testAuthMethod.PublicId, "valid-3"), + overrides: func(c *ClaimsScope) { c.Scope = "bad-scope" }, + wantErr: false, + wantRowsDeleted: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + cp := tt.claimsScope.Clone() + require.NoError(rw.Create(ctx, &cp)) + + if tt.overrides != nil { + tt.overrides(cp) + } + deletedRows, err := rw.Delete(ctx, &cp) + if tt.wantErr { + require.Error(err) + return + } + require.NoError(err) + if tt.wantRowsDeleted == 0 { + assert.Equal(tt.wantRowsDeleted, deletedRows) + return + } + assert.Equal(tt.wantRowsDeleted, deletedRows) + found := AllocClaimsScope() + err = rw.LookupWhere(ctx, &found, "oidc_method_id = ? and scope = ?", tt.claimsScope.OidcMethodId, tt.claimsScope.Scope) + assert.True(errors.IsNotFoundError(err)) + }) + } +} + +func TestClaimsScope_Clone(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + + t.Run("valid", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + m := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "my-dogs-name", + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + orig, err := NewClaimsScope(m.PublicId, "profile") + require.NoError(err) + cp := orig.Clone() + assert.True(proto.Equal(cp.ClaimsScope, orig.ClaimsScope)) + }) + t.Run("not-equal", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + m := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "my-dogs-name", + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + orig, err := NewClaimsScope(m.PublicId, "email") + require.NoError(err) + orig2, err := NewClaimsScope(m.PublicId, "profile") + require.NoError(err) + + cp := orig.Clone() + assert.True(!proto.Equal(cp.ClaimsScope, orig2.ClaimsScope)) + }) +} + +func TestClaimsScope_SetTableName(t *testing.T) { + t.Parallel() + defaultTableName := defaultClaimsScopeTableName + tests := []struct { + name string + setNameTo string + want string + }{ + { + name: "new-name", + setNameTo: "new-name", + want: "new-name", + }, + { + name: "reset to default", + setNameTo: "", + want: defaultTableName, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + def := AllocClaimsScope() + require.Equal(defaultTableName, def.TableName()) + m := AllocClaimsScope() + m.SetTableName(tt.setNameTo) + assert.Equal(tt.want, m.TableName()) + }) + } +} diff --git a/internal/auth/oidc/immutable_fields_test.go b/internal/auth/oidc/immutable_fields_test.go index 5799bf8e3..4b57f676a 100644 --- a/internal/auth/oidc/immutable_fields_test.go +++ b/internal/auth/oidc/immutable_fields_test.go @@ -391,7 +391,7 @@ func TestAccount_ImmutableFields(t *testing.T) { cp.Issuer = "bob.com" return cp }(), - fieldMask: []string{"Issuer"}, + fieldMask: []string{IssuerField}, }, { name: "subject", diff --git a/internal/auth/oidc/options.go b/internal/auth/oidc/options.go index fa52070cb..fc4718ba0 100644 --- a/internal/auth/oidc/options.go +++ b/internal/auth/oidc/options.go @@ -27,6 +27,7 @@ type options struct { withCertificates []*x509.Certificate withAudClaims []string withSigningAlgs []Alg + withClaimsScopes []string withEmail string withFullName string withOrderByCreateTime bool @@ -112,6 +113,13 @@ func WithSigningAlgs(alg ...Alg) Option { } } +// WithClaimsScopes provides optional claims scopes +func WithClaimsScopes(claimsScope ...string) Option { + return func(o *options) { + o.withClaimsScopes = claimsScope + } +} + // WithEmail provides an optional email address for the account. func WithEmail(email string) Option { return func(o *options) { diff --git a/internal/auth/oidc/options_test.go b/internal/auth/oidc/options_test.go index f3c6452eb..a033462a3 100644 --- a/internal/auth/oidc/options_test.go +++ b/internal/auth/oidc/options_test.go @@ -85,6 +85,14 @@ func Test_GetOpts(t *testing.T) { testOpts.withSigningAlgs = algs assert.Equal(opts, testOpts) }) + t.Run("WithClaimsScopes", func(t *testing.T) { + assert := assert.New(t) + scopes := []string{"email", "profile"} + opts := getOpts(WithClaimsScopes(scopes...)) + testOpts := getDefaultOptions() + testOpts.withClaimsScopes = scopes + assert.Equal(opts, testOpts) + }) t.Run("WithEmail", func(t *testing.T) { assert := assert.New(t) opts := getOpts(WithEmail("bob@alice.com")) diff --git a/internal/auth/oidc/repository_account.go b/internal/auth/oidc/repository_account.go index e8732a078..70ea38cce 100644 --- a/internal/auth/oidc/repository_account.go +++ b/internal/auth/oidc/repository_account.go @@ -213,8 +213,8 @@ func (r *Repository) UpdateAccount(ctx context.Context, scopeId string, a *Accou for _, f := range fieldMaskPaths { switch { - case strings.EqualFold("Name", f): - case strings.EqualFold("Description", f): + case strings.EqualFold(NameField, f): + case strings.EqualFold(DescriptionField, f): default: return nil, db.NoRowsAffected, errors.New(errors.InvalidFieldMask, op, f) } @@ -222,8 +222,8 @@ func (r *Repository) UpdateAccount(ctx context.Context, scopeId string, a *Accou var dbMask, nullFields []string dbMask, nullFields = dbcommon.BuildUpdatePaths( map[string]interface{}{ - "Name": a.Name, - "Description": a.Description, + NameField: a.Name, + DescriptionField: a.Description, }, fieldMaskPaths, nil, diff --git a/internal/auth/oidc/repository_account_test.go b/internal/auth/oidc/repository_account_test.go index 51f763d8d..8cda9fed3 100644 --- a/internal/auth/oidc/repository_account_test.go +++ b/internal/auth/oidc/repository_account_test.go @@ -458,7 +458,7 @@ func TestRepository_CreateAccount_DuplicateFields(t *testing.T) { assert.Equal(got.CreateTime, got.UpdateTime) authMethod.Issuer = "https://somethingelse.com" - authMethod, _, err = repo.UpdateAuthMethod(ctx, authMethod, authMethod.Version, []string{"Issuer"}, WithForce()) + authMethod, _, err = repo.UpdateAuthMethod(ctx, authMethod, authMethod.Version, []string{IssuerField}, WithForce()) require.NoError(err) got2, err := repo.CreateAccount(context.Background(), org.GetPublicId(), in2) @@ -881,7 +881,7 @@ func TestRepository_UpdateAccount(t *testing.T) { Account: &store.Account{}, }, chgFn: makeNil(), - masks: []string{"Name", "Description"}, + masks: []string{NameField, DescriptionField}, wantIsErr: errors.InvalidParameter, wantErrMsg: "oidc.(Repository).UpdateAccount: missing Account: parameter violation: error #100", }, @@ -893,7 +893,7 @@ func TestRepository_UpdateAccount(t *testing.T) { Account: &store.Account{}, }, chgFn: makeEmbeddedNil(), - masks: []string{"Name", "Description"}, + masks: []string{NameField, DescriptionField}, wantIsErr: errors.InvalidParameter, wantErrMsg: "oidc.(Repository).UpdateAccount: missing embedded Account: parameter violation: error #100", }, @@ -906,7 +906,7 @@ func TestRepository_UpdateAccount(t *testing.T) { }, }, chgFn: changeName("no-scope-id-test-update-name-repo"), - masks: []string{"Name"}, + masks: []string{NameField}, wantIsErr: errors.InvalidParameter, wantErrMsg: "oidc.(Repository).UpdateAccount: missing scope id: parameter violation: error #100", }, @@ -919,7 +919,7 @@ func TestRepository_UpdateAccount(t *testing.T) { }, }, chgFn: changeName("test-update-name-repo"), - masks: []string{"Name"}, + masks: []string{NameField}, wantIsErr: errors.InvalidParameter, wantErrMsg: "oidc.(Repository).UpdateAccount: missing version: parameter violation: error #100", }, @@ -931,7 +931,7 @@ func TestRepository_UpdateAccount(t *testing.T) { Account: &store.Account{}, }, chgFn: deletePublicId(), - masks: []string{"Name", "Description"}, + masks: []string{NameField, DescriptionField}, wantIsErr: errors.InvalidPublicId, wantErrMsg: "oidc.(Repository).UpdateAccount: missing public id: parameter violation: error #102", }, @@ -945,7 +945,7 @@ func TestRepository_UpdateAccount(t *testing.T) { }, }, chgFn: combine(nonExistentPublicId(), changeName("updating-non-existent-Account-test-update-name-repo")), - masks: []string{"Name"}, + masks: []string{NameField}, wantIsErr: errors.RecordNotFound, wantErrMsg: "oidc.(Repository).UpdateAccount: abcd_OOOOOOOOOO: db.DoTx: oidc.(Repository).UpdateAccount: db.Update: db.lookupAfterWrite: db.LookupById: record not found, search issue: error #1100", }, @@ -1000,7 +1000,7 @@ func TestRepository_UpdateAccount(t *testing.T) { }, }, chgFn: changeName("change-name-test-update-name-repo"), - masks: []string{"Name"}, + masks: []string{NameField}, want: &Account{ Account: &store.Account{ Name: "change-name-test-update-name-repo", @@ -1018,7 +1018,7 @@ func TestRepository_UpdateAccount(t *testing.T) { }, }, chgFn: changeDescription("test-update-description-repo"), - masks: []string{"Description"}, + masks: []string{DescriptionField}, want: &Account{ Account: &store.Account{ Description: "test-update-description-repo", @@ -1037,7 +1037,7 @@ func TestRepository_UpdateAccount(t *testing.T) { }, }, chgFn: combine(changeDescription("test-update-description-repo"), changeName("change-name-and-description-test-update-name-repo")), - masks: []string{"Name", "Description"}, + masks: []string{NameField, DescriptionField}, want: &Account{ Account: &store.Account{ Name: "change-name-and-description-test-update-name-repo", @@ -1056,7 +1056,7 @@ func TestRepository_UpdateAccount(t *testing.T) { Description: "test-description-repo", }, }, - masks: []string{"Name"}, + masks: []string{NameField}, chgFn: combine(changeDescription("test-update-description-repo"), changeName("")), want: &Account{ Account: &store.Account{ @@ -1075,7 +1075,7 @@ func TestRepository_UpdateAccount(t *testing.T) { Description: "test-description-repo", }, }, - masks: []string{"Description"}, + masks: []string{DescriptionField}, chgFn: combine(changeDescription(""), changeName("test-update-name-repo")), want: &Account{ Account: &store.Account{ @@ -1094,7 +1094,7 @@ func TestRepository_UpdateAccount(t *testing.T) { Description: "test-description-repo", }, }, - masks: []string{"Description"}, + masks: []string{DescriptionField}, chgFn: combine(changeDescription("test-update-description-repo"), changeName("")), want: &Account{ Account: &store.Account{ @@ -1114,7 +1114,7 @@ func TestRepository_UpdateAccount(t *testing.T) { Description: "test-description-repo", }, }, - masks: []string{"Name"}, + masks: []string{NameField}, chgFn: combine(changeDescription(""), changeName("do-not-delete-description-test-update-name-repo")), want: &Account{ Account: &store.Account{ @@ -1208,7 +1208,7 @@ func TestRepository_UpdateAccount_DupeNames(t *testing.T) { ab := TestAccount(t, conn, am, "create-success2") aa.Name = name - got1, gotCount1, err := repo.UpdateAccount(context.Background(), org.GetPublicId(), aa, 1, []string{"name"}) + got1, gotCount1, err := repo.UpdateAccount(context.Background(), org.GetPublicId(), aa, 1, []string{NameField}) assert.NoError(err) require.NotNil(got1) assert.Equal(name, got1.Name) @@ -1216,7 +1216,7 @@ func TestRepository_UpdateAccount_DupeNames(t *testing.T) { assert.NoError(db.TestVerifyOplog(t, rw, aa.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))) ab.Name = name - got2, gotCount2, err := repo.UpdateAccount(context.Background(), org.GetPublicId(), ab, 1, []string{"name"}) + got2, gotCount2, err := repo.UpdateAccount(context.Background(), org.GetPublicId(), ab, 1, []string{NameField}) assert.Truef(errors.Match(errors.T(errors.NotUnique), err), "Unexpected error %s", err) assert.Nil(got2) assert.Equal(db.NoRowsAffected, gotCount2, "row count") @@ -1254,7 +1254,7 @@ func TestRepository_UpdateAccount_DupeNames(t *testing.T) { ab := TestAccount(t, conn, amb, "create-success2", WithName("test-name-ab")) ab.Name = aa.Name - got3, gotCount3, err := repo.UpdateAccount(context.Background(), org.GetPublicId(), ab, 1, []string{"name"}) + got3, gotCount3, err := repo.UpdateAccount(context.Background(), org.GetPublicId(), ab, 1, []string{NameField}) assert.NoError(err) require.NotNil(got3) assert.NotSame(ab, got3) @@ -1297,7 +1297,7 @@ func TestRepository_UpdateAccount_DupeNames(t *testing.T) { aa.AuthMethodId = ab.AuthMethodId assert.Equal(aa.AuthMethodId, ab.AuthMethodId) - got1, gotCount1, err := repo.UpdateAccount(context.Background(), org.GetPublicId(), aa, 1, []string{"name"}) + got1, gotCount1, err := repo.UpdateAccount(context.Background(), org.GetPublicId(), aa, 1, []string{NameField}) assert.NoError(err) require.NotNil(got1) diff --git a/internal/auth/oidc/repository_auth_method.go b/internal/auth/oidc/repository_auth_method.go index 959c3c931..5024bbf37 100644 --- a/internal/auth/oidc/repository_auth_method.go +++ b/internal/auth/oidc/repository_auth_method.go @@ -59,7 +59,7 @@ func (r *Repository) upsertAccount(ctx context.Context, am *AuthMethod, IdTokenC columns, values = append(columns, "full_name"), append(values, foundName) default: conflictClauses = append(conflictClauses, "full_name = NULL") - nullMasks = append(nullMasks, "Name") + nullMasks = append(nullMasks, NameField) } switch { case AccessTokenClaims["email"] != nil: @@ -76,7 +76,7 @@ func (r *Repository) upsertAccount(ctx context.Context, am *AuthMethod, IdTokenC if foundName != nil { values = append(values, foundName) conflictClauses = append(conflictClauses, fmt.Sprintf("full_name = $%d", len(values))) - fieldMasks = append(fieldMasks, "Name") + fieldMasks = append(fieldMasks, NameField) } if foundEmail != nil { values = append(values, foundEmail) diff --git a/internal/auth/oidc/repository_auth_method_create.go b/internal/auth/oidc/repository_auth_method_create.go index d677ba442..d5551360a 100644 --- a/internal/auth/oidc/repository_auth_method_create.go +++ b/internal/auth/oidc/repository_auth_method_create.go @@ -105,6 +105,13 @@ func (r *Repository) CreateAuthMethod(ctx context.Context, am *AuthMethod, opt . } msgs = append(msgs, certOplogMsgs...) } + if len(vo.ClaimsScopes) > 0 { + scopesOplogMsgs := make([]*oplog.Message, 0, len(vo.ClaimsScopes)) + if err := w.CreateItems(ctx, vo.ClaimsScopes, db.NewOplogMsgs(&scopesOplogMsgs)); err != nil { + return err + } + msgs = append(msgs, scopesOplogMsgs...) + } metadata := am.oplog(oplog.OpType_OP_TYPE_CREATE) if err := w.WriteOplogEntryWith(ctx, oplogWrapper, ticket, metadata, msgs); err != nil { diff --git a/internal/auth/oidc/repository_auth_method_create_test.go b/internal/auth/oidc/repository_auth_method_create_test.go index ef73ac90b..e246e037b 100644 --- a/internal/auth/oidc/repository_auth_method_create_test.go +++ b/internal/auth/oidc/repository_auth_method_create_test.go @@ -58,6 +58,7 @@ func TestRepository_CreateAuthMethod(t *testing.T) { WithCertificates(certs...), WithName("alice's restaurant"), WithDescription("it's a good place to eat"), + WithClaimsScopes("email", "profile"), ) require.NoError(t, err) require.Equal(t, am.SigningAlgs, convertAlg(algs...)) @@ -90,6 +91,7 @@ func TestRepository_CreateAuthMethod(t *testing.T) { WithCertificates(certs...), WithName("alice's restaurant with a twist"), WithDescription("it's an okay but kinda weird place to eat"), + WithClaimsScopes("email", "profile"), ) require.NoError(t, err) require.Equal(t, am.SigningAlgs, convertAlg(algs...)) @@ -123,6 +125,7 @@ func TestRepository_CreateAuthMethod(t *testing.T) { WithCertificates(certs...), WithName("alice's restaurant is bad"), WithDescription("their food is awful"), + WithClaimsScopes("email", "profile"), ) require.NoError(t, err) require.Equal(t, am.SigningAlgs, convertAlg(algs...)) diff --git a/internal/auth/oidc/repository_auth_method_operational_state.go b/internal/auth/oidc/repository_auth_method_operational_state.go index 257acb190..a1e86abde 100644 --- a/internal/auth/oidc/repository_auth_method_operational_state.go +++ b/internal/auth/oidc/repository_auth_method_operational_state.go @@ -98,7 +98,7 @@ func (r *Repository) transitionAuthMethodTo(ctx context.Context, authMethodId st updatedAm = am.Clone() updatedAm.OperationalState = string(desiredState) updatedAm.DisableDiscoveredConfigValidation = opts.withForce - dbMask := []string{"OperationalState", "DisableDiscoveredConfigValidation"} + dbMask := []string{OperationalStateField, DisableDiscoveredConfigValidationField} rowsUpdated, err := w.Update(ctx, updatedAm, dbMask, nil, db.WithOplog(oplogWrapper, updatedAm.oplog(oplog.OpType_OP_TYPE_UPDATE)), db.WithVersion(&version)) switch { case err != nil: diff --git a/internal/auth/oidc/repository_auth_method_read.go b/internal/auth/oidc/repository_auth_method_read.go index dde0f41ed..8169409c6 100644 --- a/internal/auth/oidc/repository_auth_method_read.go +++ b/internal/auth/oidc/repository_auth_method_read.go @@ -158,6 +158,9 @@ func (r *Repository) getAuthMethods(ctx context.Context, authMethodId string, sc if agg.Certs != "" { am.Certificates = strings.Split(agg.Certs, aggregateDelimiter) } + if agg.ClaimsScopes != "" { + am.ClaimsScopes = strings.Split(agg.ClaimsScopes, aggregateDelimiter) + } authMethods = append(authMethods, &am) } return authMethods, nil @@ -187,6 +190,7 @@ type authMethodAgg struct { ApiUrl string Auds string Certs string + ClaimsScopes string } // TableName returns the table name for gorm diff --git a/internal/auth/oidc/repository_auth_method_read_test.go b/internal/auth/oidc/repository_auth_method_read_test.go index 7398bf746..c41e25d36 100644 --- a/internal/auth/oidc/repository_auth_method_read_test.go +++ b/internal/auth/oidc/repository_auth_method_read_test.go @@ -35,7 +35,9 @@ func TestRepository_LookupAuthMethod(t *testing.T) { conn, databaseWrapper, org.PublicId, ActivePublicState, "alice_rp", "alices-dogs-name", WithApiUrl(TestConvertToUrls(t, "https://alice-active-pub.com/callback")[0]), - WithSigningAlgs(RS256)) + WithSigningAlgs(RS256), + WithClaimsScopes("email", "profile"), + ) amActivePub.IsPrimaryAuthMethod = true iam.TestSetPrimaryAuthMethod(t, iam.TestRepo(t, conn, wrapper), org, amActivePub.PublicId) @@ -128,12 +130,12 @@ func TestRepository_ListAuthMethods(t *testing.T) { require.NoError(t, err) am1a := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "alices-dogs-name", - WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0]), WithClaimsScopes("email", "profile")) iam.TestSetPrimaryAuthMethod(t, iamRepo, org, am1a.PublicId) am1a.IsPrimaryAuthMethod = true _ = TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp-2", "alices-cat-name", - WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0]), WithClaimsScopes("email", "profile")) return []string{am1a.ScopeId}, []*AuthMethod{am1a}, am1a.PublicId }, @@ -229,6 +231,7 @@ func TestRepository_getAuthMethods(t *testing.T) { WithApiUrl(TestConvertToUrls(t, "https://alice.com/callback")[0]), WithSigningAlgs(RS256, ES256), WithCertificates(cert1, cert2), + WithClaimsScopes("email", "profile"), ) am1b := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp-2", "alices-cat-name", WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) diff --git a/internal/auth/oidc/repository_auth_method_update.go b/internal/auth/oidc/repository_auth_method_update.go index 292da6b14..1a483c200 100644 --- a/internal/auth/oidc/repository_auth_method_update.go +++ b/internal/auth/oidc/repository_auth_method_update.go @@ -15,6 +15,25 @@ import ( "github.com/hashicorp/go-multierror" ) +const ( + OperationalStateField = "OperationalState" + DisableDiscoveredConfigValidationField = "DisableDiscoveredConfigValidation" + VersionField = "Version" + NameField = "Name" + DescriptionField = "Description" + IssuerField = "Issuer" + ClientIdField = "ClientId" + ClientSecretField = "ClientSecret" + CtClientSecretField = "CtClientSecret" + ClientSecretHmacField = "ClientSecretHmac" + MaxAgeField = "MaxAge" + SigningAlgsField = "SigningAlgs" + ApiUrlField = "ApiUrl" + AudClaimsField = "AudClaims" + CertificatesField = "Certificates" + ClaimsScopesField = "ClaimsScopes" +) + // UpdateAuthMethod will retrieve the auth method from the repository, // and update it based on the field masks provided. // @@ -70,16 +89,17 @@ func (r *Repository) UpdateAuthMethod(ctx context.Context, am *AuthMethod, versi dbMask, nullFields := dbcommon.BuildUpdatePaths( map[string]interface{}{ - "Name": am.Name, - "Description": am.Description, - "Issuer": am.Issuer, - "ClientId": am.ClientId, - "ClientSecret": am.ClientSecret, - "MaxAge": am.MaxAge, - "SigningAlgs": am.SigningAlgs, - "ApiUrl": am.ApiUrl, - "AudClaims": am.AudClaims, - "Certificates": am.Certificates, + NameField: am.Name, + DescriptionField: am.Description, + IssuerField: am.Issuer, + ClientIdField: am.ClientId, + ClientSecretField: am.ClientSecret, + MaxAgeField: am.MaxAge, + SigningAlgsField: am.SigningAlgs, + ApiUrlField: am.ApiUrl, + AudClaimsField: am.AudClaims, + CertificatesField: am.Certificates, + ClaimsScopesField: am.ClaimsScopes, }, fieldMaskPaths, nil, @@ -139,10 +159,15 @@ func (r *Repository) UpdateAuthMethod(ctx context.Context, am *AuthMethod, versi return nil, db.NoRowsAffected, errors.Wrap(err, op) } + addScopes, deleteScopes, err := valueObjectChanges(origAm.PublicId, ClaimsScopesVO, am.ClaimsScopes, origAm.ClaimsScopes, dbMask, nullFields) + if err != nil { + return nil, db.NoRowsAffected, errors.Wrap(err, op) + } + var filteredDbMask, filteredNullFields []string for _, f := range dbMask { switch f { - case "SigningAlgs", "CallbackUrls", "AudClaims", "Certificates": + case SigningAlgsField, AudClaimsField, CertificatesField, ClaimsScopesField: continue default: filteredDbMask = append(filteredDbMask, f) @@ -150,7 +175,7 @@ func (r *Repository) UpdateAuthMethod(ctx context.Context, am *AuthMethod, versi } for _, f := range nullFields { switch f { - case "SigningAlgs", "CallbackUrls", "AudClaims", "Certificates": + case SigningAlgsField, AudClaimsField, CertificatesField, ClaimsScopesField: continue default: filteredNullFields = append(filteredNullFields, f) @@ -165,17 +190,19 @@ func (r *Repository) UpdateAuthMethod(ctx context.Context, am *AuthMethod, versi len(addCerts) == 0 && len(deleteCerts) == 0 && len(addAuds) == 0 && - len(deleteAuds) == 0 { + len(deleteAuds) == 0 && + len(addScopes) == 0 && + len(deleteScopes) == 0 { return origAm, db.NoRowsAffected, nil } // ClientSecret is a bit odd, because it uses the Struct wrapping, we need // to add the encrypted fields to the dbMask or nullFields - if strutil.StrListContains(filteredDbMask, "ClientSecret") { - filteredDbMask = append(filteredDbMask, "CtClientSecret", "ClientSecretHmac") + if strutil.StrListContains(filteredDbMask, ClientSecretField) { + filteredDbMask = append(filteredDbMask, CtClientSecretField, ClientSecretHmacField) } - if strutil.StrListContains(filteredNullFields, "ClientSecret") { - filteredNullFields = append(filteredNullFields, "CtClientSecret", "ClientSecretHmac") + if strutil.StrListContains(filteredNullFields, ClientSecretField) { + filteredNullFields = append(filteredNullFields, CtClientSecretField, ClientSecretHmacField) } databaseWrapper, err := r.kms.GetWrapper(ctx, origAm.ScopeId, kms.KeyPurposeDatabase) @@ -213,7 +240,7 @@ func (r *Repository) UpdateAuthMethod(ctx context.Context, am *AuthMethod, versi // method's version. updatedAm = am.Clone() updatedAm.Version = uint32(version) + 1 - rowsUpdated, err = w.Update(ctx, updatedAm, []string{"Version", "DisableDiscoveredConfigValidation"}, nil, db.NewOplogMsg(&authMethodOplogMsg), db.WithVersion(&version)) + rowsUpdated, err = w.Update(ctx, updatedAm, []string{VersionField, DisableDiscoveredConfigValidationField}, nil, db.NewOplogMsg(&authMethodOplogMsg), db.WithVersion(&version)) if err != nil { return errors.Wrap(err, op, errors.WithMsg("unable to update auth method version")) } @@ -221,7 +248,7 @@ func (r *Repository) UpdateAuthMethod(ctx context.Context, am *AuthMethod, versi return errors.New(errors.MultipleRecords, op, fmt.Sprintf("updated auth method version and %d rows updated", rowsUpdated)) } default: - filteredDbMask = append(filteredDbMask, "DisableDiscoveredConfigValidation") + filteredDbMask = append(filteredDbMask, DisableDiscoveredConfigValidationField) updatedAm = am.Clone() rowsUpdated, err = w.Update(ctx, updatedAm, filteredDbMask, filteredNullFields, db.NewOplogMsg(&authMethodOplogMsg), db.WithVersion(&version)) if err != nil { @@ -275,7 +302,7 @@ func (r *Repository) UpdateAuthMethod(ctx context.Context, am *AuthMethod, versi deleteAudsOplogMsgs := make([]*oplog.Message, 0, len(deleteAuds)) rowsDeleted, err := w.DeleteItems(ctx, deleteAuds, db.NewOplogMsgs(&deleteAudsOplogMsgs)) if err != nil { - return errors.Wrap(err, op, errors.WithMsg("unable to audiences URLs")) + return errors.Wrap(err, op, errors.WithMsg("unable to delete audiences URLs")) } if rowsDeleted != len(deleteAuds) { return errors.New(errors.MultipleRecords, op, fmt.Sprintf("audiences deleted %d did not match request for %d", rowsDeleted, len(deleteAuds))) @@ -290,6 +317,25 @@ func (r *Repository) UpdateAuthMethod(ctx context.Context, am *AuthMethod, versi msgs = append(msgs, addAudsOplogMsgs...) } + if len(deleteScopes) > 0 { + deleteScopesOplogMsgs := make([]*oplog.Message, 0, len(deleteScopes)) + rowsDeleted, err := w.DeleteItems(ctx, deleteScopes, db.NewOplogMsgs(&deleteScopesOplogMsgs)) + if err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to delete claims scopes")) + } + if rowsDeleted != len(deleteScopes) { + return errors.New(errors.MultipleRecords, op, fmt.Sprintf("claims scopes deleted %d did not match request for %d", rowsDeleted, len(deleteScopes))) + } + msgs = append(msgs, deleteScopesOplogMsgs...) + } + if len(addScopes) > 0 { + addScopesOplogMsgs := make([]*oplog.Message, 0, len(addScopes)) + if err := w.CreateItems(ctx, addScopes, db.NewOplogMsgs(&addScopesOplogMsgs)); err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to add claims scopes")) + } + msgs = append(msgs, addScopesOplogMsgs...) + } + metadata := updatedAm.oplog(oplog.OpType_OP_TYPE_UPDATE) if err := w.WriteOplogEntryWith(ctx, oplogWrapper, ticket, metadata, msgs); err != nil { return errors.Wrap(err, op, errors.WithMsg("unable to write oplog")) @@ -325,15 +371,16 @@ func (r *Repository) UpdateAuthMethod(ctx context.Context, am *AuthMethod, versi type voName string const ( - SigningAlgVO voName = "SigningAlgs" - CertificateVO voName = "Certificates" - AudClaimVO voName = "AudClaims" + SigningAlgVO voName = "SigningAlgs" + CertificateVO voName = "Certificates" + AudClaimVO voName = "AudClaims" + ClaimsScopesVO voName = "ClaimsScopes" ) // validVoName decides if the name is valid func validVoName(name voName) bool { switch name { - case SigningAlgVO, CertificateVO, AudClaimVO: + case SigningAlgVO, CertificateVO, AudClaimVO, ClaimsScopesVO: return true default: return false @@ -357,6 +404,10 @@ var supportedFactories = map[voName]factoryFunc{ str := fmt.Sprintf("%s", i) return NewAudClaim(publicId, str) }, + ClaimsScopesVO: func(publicId string, i interface{}) (interface{}, error) { + str := fmt.Sprintf("%s", i) + return NewClaimsScope(publicId, str) + }, } // valueObjectChanges takes the new and old list of VOs (value objects) and @@ -446,16 +497,17 @@ func validateFieldMask(fieldMaskPaths []string) error { const op = "validateFieldMask" for _, f := range fieldMaskPaths { switch { - case strings.EqualFold("Name", f): - case strings.EqualFold("Description", f): - case strings.EqualFold("Issuer", f): - case strings.EqualFold("ClientId", f): - case strings.EqualFold("ClientSecret", f): - case strings.EqualFold("MaxAge", f): - case strings.EqualFold("SigningAlgs", f): - case strings.EqualFold("ApiUrl", f): - case strings.EqualFold("AudClaims", f): - case strings.EqualFold("Certificates", f): + case strings.EqualFold(NameField, f): + case strings.EqualFold(DescriptionField, f): + case strings.EqualFold(IssuerField, f): + case strings.EqualFold(ClientIdField, f): + case strings.EqualFold(ClientSecretField, f): + case strings.EqualFold(MaxAgeField, f): + case strings.EqualFold(SigningAlgsField, f): + case strings.EqualFold(ApiUrlField, f): + case strings.EqualFold(AudClaimsField, f): + case strings.EqualFold(CertificatesField, f): + case strings.EqualFold(ClaimsScopesField, f): default: return errors.New(errors.InvalidParameter, op, fmt.Sprintf("invalid field mask: %s", f)) } @@ -468,21 +520,21 @@ func applyUpdate(new, orig *AuthMethod, fieldMaskPaths []string) *AuthMethod { cp := orig.Clone() for _, f := range fieldMaskPaths { switch f { - case "Name": + case NameField: cp.Name = new.Name - case "Description": + case DescriptionField: cp.Description = new.Description - case "Issuer": + case IssuerField: cp.Issuer = new.Issuer - case "ClientId": + case ClientIdField: cp.ClientId = new.ClientId - case "ClientSecret": + case ClientSecretField: cp.ClientSecret = new.ClientSecret - case "MaxAge": + case MaxAgeField: cp.MaxAge = new.MaxAge - case "ApiUrl": + case ApiUrlField: cp.ApiUrl = new.ApiUrl - case "SigningAlgs": + case SigningAlgsField: switch { case len(new.SigningAlgs) == 0: cp.SigningAlgs = nil @@ -490,7 +542,7 @@ func applyUpdate(new, orig *AuthMethod, fieldMaskPaths []string) *AuthMethod { cp.SigningAlgs = make([]string, 0, len(new.SigningAlgs)) cp.SigningAlgs = append(cp.SigningAlgs, new.SigningAlgs...) } - case "AudClaims": + case AudClaimsField: switch { case len(new.AudClaims) == 0: cp.AudClaims = nil @@ -498,7 +550,7 @@ func applyUpdate(new, orig *AuthMethod, fieldMaskPaths []string) *AuthMethod { cp.AudClaims = make([]string, 0, len(new.AudClaims)) cp.AudClaims = append(cp.AudClaims, new.AudClaims...) } - case "Certificates": + case CertificatesField: switch { case len(new.Certificates) == 0: cp.Certificates = nil @@ -506,6 +558,14 @@ func applyUpdate(new, orig *AuthMethod, fieldMaskPaths []string) *AuthMethod { cp.Certificates = make([]string, 0, len(new.Certificates)) cp.Certificates = append(cp.Certificates, new.Certificates...) } + case ClaimsScopesField: + switch { + case len(new.ClaimsScopes) == 0: + cp.ClaimsScopes = nil + default: + cp.ClaimsScopes = make([]string, 0, len(new.ClaimsScopes)) + cp.ClaimsScopes = append(cp.ClaimsScopes, new.ClaimsScopes...) + } } } return cp diff --git a/internal/auth/oidc/repository_auth_method_update_test.go b/internal/auth/oidc/repository_auth_method_update_test.go index 06a56e289..435c0e825 100644 --- a/internal/auth/oidc/repository_auth_method_update_test.go +++ b/internal/auth/oidc/repository_auth_method_update_test.go @@ -67,6 +67,7 @@ func Test_UpdateAuthMethod(t *testing.T) { "alice-rp", "alice-secret", WithCertificates(tpCert[0]), WithSigningAlgs(Alg(tpAlg)), + WithClaimsScopes("email", "profile"), WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), ) }, @@ -79,7 +80,7 @@ func Test_UpdateAuthMethod(t *testing.T) { am.ClientSecret = "This is a new secret" return &am }, - fieldMasks: []string{"Name", "Description", "AudClaims", "ClientSecret"}, + fieldMasks: []string{NameField, DescriptionField, AudClaimsField, ClientSecretField}, version: 1, want: func(orig, updateWith *AuthMethod) *AuthMethod { am := orig.Clone() @@ -106,6 +107,7 @@ func Test_UpdateAuthMethod(t *testing.T) { WithAudClaims("www.alice.com"), WithCertificates(tpCert[0]), WithSigningAlgs(Alg(tpAlg)), + WithClaimsScopes("email", "profile"), WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), ) }, @@ -119,9 +121,10 @@ func Test_UpdateAuthMethod(t *testing.T) { am.AudClaims = []string{"www.alice.com/admin"} am.SigningAlgs = []string{string(ES384), string(ES512)} am.Certificates = []string{pem} + am.ClaimsScopes = []string{"custom-scope1", "email"} return &am }, - fieldMasks: []string{"Name", "Description", "AudClaims", "ApiUrl", "SigningAlgs", "Certificates"}, + fieldMasks: []string{NameField, DescriptionField, AudClaimsField, ApiUrlField, SigningAlgsField, CertificatesField, ClaimsScopesField}, version: 1, opt: []Option{WithForce()}, want: func(orig, updateWith *AuthMethod) *AuthMethod { @@ -132,6 +135,7 @@ func Test_UpdateAuthMethod(t *testing.T) { am.AudClaims = updateWith.AudClaims am.SigningAlgs = updateWith.SigningAlgs am.Certificates = updateWith.Certificates + am.ClaimsScopes = updateWith.ClaimsScopes am.DisableDiscoveredConfigValidation = true return am }, @@ -151,7 +155,7 @@ func Test_UpdateAuthMethod(t *testing.T) { am.Description = "" return &am }, - fieldMasks: []string{"Name", "Description"}, + fieldMasks: []string{NameField, DescriptionField}, version: 1, want: func(orig, updateWith *AuthMethod) *AuthMethod { am := orig.Clone() @@ -173,7 +177,7 @@ func Test_UpdateAuthMethod(t *testing.T) { am.PublicId = orig.PublicId return &am }, - fieldMasks: []string{"SigningAlgs"}, + fieldMasks: []string{SigningAlgsField}, version: 1, want: func(orig, updateWith *AuthMethod) *AuthMethod { am := orig.Clone() @@ -181,6 +185,31 @@ func Test_UpdateAuthMethod(t *testing.T) { return am }, }, + { + name: "null-claims-scope", + setup: func() *AuthMethod { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + return TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice-rp", "alice-secret", + WithCertificates(tpCert[0]), + WithSigningAlgs(Alg(tpAlg)), + WithClaimsScopes("email", "profile"), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0])) + }, + updateWith: func(orig *AuthMethod) *AuthMethod { + am := AllocAuthMethod() + am.PublicId = orig.PublicId + return &am + }, + fieldMasks: []string{ClaimsScopesField}, + version: 1, + want: func(orig, updateWith *AuthMethod) *AuthMethod { + am := orig.Clone() + am.ClaimsScopes = nil + return am + }, + }, { name: "change-callback-url", setup: func() *AuthMethod { @@ -195,7 +224,7 @@ func Test_UpdateAuthMethod(t *testing.T) { am.ApiUrl = "https://www.updated.com/callback" return &am }, - fieldMasks: []string{"ApiUrl"}, + fieldMasks: []string{ApiUrlField}, version: 1, want: func(orig, updateWith *AuthMethod) *AuthMethod { am := orig.Clone() @@ -209,7 +238,10 @@ func Test_UpdateAuthMethod(t *testing.T) { org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) require.NoError(t, err) - return TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice-rp", "alice-secret", WithCertificates(tpCert[0]), WithSigningAlgs(Alg(tpAlg)), WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0])) + return TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice-rp", "alice-secret", + WithCertificates(tpCert[0]), + WithSigningAlgs(Alg(tpAlg)), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0])) }, updateWith: func(orig *AuthMethod) *AuthMethod { am := AllocAuthMethod() @@ -217,7 +249,7 @@ func Test_UpdateAuthMethod(t *testing.T) { am.SigningAlgs = []string{string(tpAlg)} return &am }, - fieldMasks: []string{"SigningAlgs"}, + fieldMasks: []string{SigningAlgsField}, version: 1, want: func(orig, updateWith *AuthMethod) *AuthMethod { am := orig.Clone() @@ -241,7 +273,7 @@ func Test_UpdateAuthMethod(t *testing.T) { am.AudClaims = []string{"www.alice.com", "www.alice.com/admin"} return &am }, - fieldMasks: []string{"Name", "Description", "AudClaims"}, + fieldMasks: []string{NameField, DescriptionField, AudClaimsField}, version: 1, want: func(orig, updateWith *AuthMethod) *AuthMethod { am := orig.Clone() @@ -265,6 +297,7 @@ func Test_UpdateAuthMethod(t *testing.T) { WithAudClaims("www.alice.com"), WithCertificates(tpCert[0]), WithSigningAlgs(Alg(tpAlg)), + WithClaimsScopes("email", "profile"), WithIssuer(TestConvertToUrls(t, tp.Addr())[0]), WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), ) @@ -276,10 +309,11 @@ func Test_UpdateAuthMethod(t *testing.T) { am.Description = "the best place to eat" am.AudClaims = []string{"www.alice.com/admin"} am.ApiUrl = "https://www.bob.com/callback" + am.ClaimsScopes = []string{"custom-scope1"} am.SigningAlgs = []string{string(ES384), string(ES512)} return &am }, - fieldMasks: []string{"Name", "Description", "AudClaims", "ApiUrl", "SigningAlgs"}, + fieldMasks: []string{NameField, DescriptionField, AudClaimsField, ApiUrlField, SigningAlgsField, ClaimsScopesField}, version: 1, opt: []Option{WithDryRun()}, want: func(orig, updateWith *AuthMethod) *AuthMethod { @@ -289,6 +323,7 @@ func Test_UpdateAuthMethod(t *testing.T) { am.AudClaims = updateWith.AudClaims am.ApiUrl = updateWith.ApiUrl am.SigningAlgs = updateWith.SigningAlgs + am.ClaimsScopes = updateWith.ClaimsScopes return am }, wantErrMatch: errors.T(errors.InvalidParameter), @@ -307,6 +342,7 @@ func Test_UpdateAuthMethod(t *testing.T) { WithAudClaims("www.alice.com"), WithCertificates(tpCert[0]), WithSigningAlgs(Alg(tpAlg)), + WithClaimsScopes("email", "profile"), WithIssuer(TestConvertToUrls(t, tp.Addr())[0]), WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), ) @@ -320,7 +356,7 @@ func Test_UpdateAuthMethod(t *testing.T) { am.ApiUrl = "https://www.bob.com/callback" return &am }, - fieldMasks: []string{"Name", "Description", "AudClaims", "ApiUrl"}, + fieldMasks: []string{NameField, DescriptionField, AudClaimsField, ApiUrlField}, version: 1, opt: []Option{WithDryRun()}, want: func(orig, updateWith *AuthMethod) *AuthMethod { @@ -336,7 +372,7 @@ func Test_UpdateAuthMethod(t *testing.T) { name: "nil-authMethod", setup: func() *AuthMethod { return nil }, updateWith: func(orig *AuthMethod) *AuthMethod { return nil }, - fieldMasks: []string{"Name"}, + fieldMasks: []string{NameField}, version: 1, wantErrMatch: errors.T(errors.InvalidParameter), }, @@ -344,7 +380,7 @@ func Test_UpdateAuthMethod(t *testing.T) { name: "nil-authMethod-store", setup: func() *AuthMethod { return nil }, updateWith: func(orig *AuthMethod) *AuthMethod { return &AuthMethod{} }, - fieldMasks: []string{"Name"}, + fieldMasks: []string{NameField}, version: 1, wantErrMatch: errors.T(errors.InvalidParameter), }, @@ -352,7 +388,7 @@ func Test_UpdateAuthMethod(t *testing.T) { name: "missing-public-id", setup: func() *AuthMethod { return nil }, updateWith: func(orig *AuthMethod) *AuthMethod { a := AllocAuthMethod(); return &a }, - fieldMasks: []string{"Name"}, + fieldMasks: []string{NameField}, version: 1, wantErrMatch: errors.T(errors.InvalidParameter), }, @@ -390,7 +426,7 @@ func Test_UpdateAuthMethod(t *testing.T) { a.PublicId = id return &a }, - fieldMasks: []string{"Name"}, + fieldMasks: []string{NameField}, version: 1, wantErrMatch: errors.T(errors.RecordNotFound), }, @@ -408,7 +444,7 @@ func Test_UpdateAuthMethod(t *testing.T) { am.Name = "alice's restaurant" return &am }, - fieldMasks: []string{"Name"}, + fieldMasks: []string{NameField}, version: 100, wantErrMatch: errors.T(errors.VersionMismatch), }, @@ -438,7 +474,7 @@ func Test_UpdateAuthMethod(t *testing.T) { am.SigningAlgs = []string{string(ES384), string(ES512)} return &am }, - fieldMasks: []string{"Name", "Description", "SigningAlgs"}, + fieldMasks: []string{NameField, DescriptionField, SigningAlgsField}, version: 1, wantErrMatch: errors.T(errors.InvalidParameter), }, @@ -455,7 +491,7 @@ func Test_UpdateAuthMethod(t *testing.T) { am.PublicId = orig.PublicId return &am }, - fieldMasks: []string{"SigningAlgs"}, + fieldMasks: []string{SigningAlgsField}, version: 2, // since TestAuthMethod(...) did an update to get it to ActivePublicState wantErrMatch: errors.T(errors.InvalidParameter), }, @@ -554,13 +590,13 @@ func Test_DisableDiscoveredConfigValidation(t *testing.T) { updateWith := am.Clone() updateWith.Name = "alice's restaurant" - updatedWithForce, rowsUpdated, err := repo.UpdateAuthMethod(ctx, updateWith, updateWith.Version, []string{"Name"}, WithForce()) + updatedWithForce, rowsUpdated, err := repo.UpdateAuthMethod(ctx, updateWith, updateWith.Version, []string{NameField}, WithForce()) require.NoError(err) assert.Equal(1, rowsUpdated) assert.Equal(true, updatedWithForce.DisableDiscoveredConfigValidation) updateWith.Name = "alice and eve's restaurant" - updatedWithoutForce, rowsUpdated, err := repo.UpdateAuthMethod(ctx, updatedWithForce, updatedWithForce.Version, []string{"Name"}) + updatedWithoutForce, rowsUpdated, err := repo.UpdateAuthMethod(ctx, updatedWithForce, updatedWithForce.Version, []string{NameField}) require.NoError(err) assert.Equal(1, rowsUpdated) assert.Equal(false, updatedWithoutForce.DisableDiscoveredConfigValidation) @@ -802,6 +838,47 @@ func Test_valueObjectChanges(t *testing.T) { return []interface{}{a, a2, a3} }(), }, + { + name: string(ClaimsScopesVO), + id: "am-public-id", + voName: ClaimsScopesVO, + new: []string{"new-scope1", "new-scope2"}, + old: []string{"old-scope1", "old-scope2", "old-scope3"}, + dbMask: []string{string(ClaimsScopesVO)}, + wantAdd: func() []interface{} { + cs, err := NewClaimsScope("am-public-id", "new-scope1") + require.NoError(t, err) + cs2, err := NewClaimsScope("am-public-id", "new-scope2") + require.NoError(t, err) + return []interface{}{cs, cs2} + }(), + wantDel: func() []interface{} { + cs, err := NewClaimsScope("am-public-id", "old-scope1") + require.NoError(t, err) + cs2, err := NewClaimsScope("am-public-id", "old-scope2") + require.NoError(t, err) + cs3, err := NewClaimsScope("am-public-id", "old-scope3") + require.NoError(t, err) + return []interface{}{cs, cs2, cs3} + }(), + }, + { + name: string(ClaimsScopesVO) + "-null-fields", + id: "am-public-id", + voName: ClaimsScopesVO, + new: nil, + old: []string{"old-scope1", "old-scope2", "old-scope3"}, + nullFields: []string{string(ClaimsScopesVO)}, + wantDel: func() []interface{} { + cs, err := NewClaimsScope("am-public-id", "old-scope1") + require.NoError(t, err) + cs2, err := NewClaimsScope("am-public-id", "old-scope2") + require.NoError(t, err) + cs3, err := NewClaimsScope("am-public-id", "old-scope3") + require.NoError(t, err) + return []interface{}{cs, cs2, cs3} + }(), + }, { name: "missing-public-id", voName: AudClaimVO, @@ -919,6 +996,12 @@ func Test_valueObjectChanges(t *testing.T) { bb := gotDel[b] return aa.(*AudClaim).Aud < bb.(*AudClaim).Aud }) + case ClaimsScopesVO: + sort.Slice(gotDel, func(a, b int) bool { + aa := gotDel[a] + bb := gotDel[b] + return aa.(*ClaimsScope).Scope < bb.(*ClaimsScope).Scope + }) } assert.Equalf(tt.wantDel, gotDel, "wantDel: %s\ngotDel: %s\n", tt.wantDel, gotDel) }) @@ -935,21 +1018,22 @@ func Test_validateFieldMask(t *testing.T) { { name: "all-valid-fields", fieldMask: []string{ - "Name", - "Description", - "Issuer", - "ClientId", - "ClientSecret", - "MaxAge", - "SigningAlgs", - "ApiUrl", - "AudClaims", - "Certificates", + NameField, + DescriptionField, + IssuerField, + ClientIdField, + ClientSecretField, + MaxAgeField, + SigningAlgsField, + ApiUrlField, + AudClaimsField, + CertificatesField, + ClaimsScopesField, }, }, { name: "invalid", - fieldMask: []string{"Invalid", "Name"}, + fieldMask: []string{"Invalid", NameField}, wantErr: true, }, } @@ -990,6 +1074,7 @@ func Test_applyUpdate(t *testing.T) { ApiUrl: "new-callback-1", AudClaims: []string{"new-aud-1", "new-aud-2"}, Certificates: []string{"new-pem1", "new-pem-2"}, + ClaimsScopes: []string{"new-scope1", "new-scope2"}, }, }, orig: &AuthMethod{ @@ -1005,6 +1090,7 @@ func Test_applyUpdate(t *testing.T) { ApiUrl: "orig-callback-1", AudClaims: []string{"orig-aud-1", "orig-aud-2"}, Certificates: []string{"orig-pem1", "orig-pem-2"}, + ClaimsScopes: []string{"orig-scope1", "orig-scope2"}, }, }, want: &AuthMethod{ @@ -1020,19 +1106,21 @@ func Test_applyUpdate(t *testing.T) { ApiUrl: "new-callback-1", AudClaims: []string{"new-aud-1", "new-aud-2"}, Certificates: []string{"new-pem1", "new-pem-2"}, + ClaimsScopes: []string{"new-scope1", "new-scope2"}, }, }, fieldMask: []string{ - "Name", - "Description", - "Issuer", - "ClientId", - "ClientSecret", - "MaxAge", - "SigningAlgs", - "ApiUrl", - "AudClaims", - "Certificates", + NameField, + DescriptionField, + IssuerField, + ClientIdField, + ClientSecretField, + MaxAgeField, + SigningAlgsField, + ApiUrlField, + AudClaimsField, + CertificatesField, + ClaimsScopesField, }, }, { @@ -1061,6 +1149,7 @@ func Test_applyUpdate(t *testing.T) { ApiUrl: "orig-callback-1", AudClaims: []string{"orig-aud-1", "orig-aud-2"}, Certificates: []string{"orig-pem1", "orig-pem-2"}, + ClaimsScopes: []string{"orig-scope1", "orig-scope2"}, }, }, want: &AuthMethod{ @@ -1075,16 +1164,17 @@ func Test_applyUpdate(t *testing.T) { }, }, fieldMask: []string{ - "Name", - "Description", - "Issuer", - "ClientId", - "ClientSecret", - "MaxAge", - "SigningAlgs", - "ApiUrl", - "AudClaims", - "Certificates", + NameField, + DescriptionField, + IssuerField, + ClientIdField, + ClientSecretField, + MaxAgeField, + SigningAlgsField, + ApiUrlField, + AudClaimsField, + CertificatesField, + ClaimsScopesField, }, }, } diff --git a/internal/auth/oidc/service_start_auth.go b/internal/auth/oidc/service_start_auth.go index 9d9d89537..a76ff9773 100644 --- a/internal/auth/oidc/service_start_auth.go +++ b/internal/auth/oidc/service_start_auth.go @@ -111,6 +111,10 @@ func StartAuth(ctx context.Context, oidcRepoFn OidcRepoFactory, authMethodId str default: } + if len(am.ClaimsScopes) > 0 { + oidcOpts = append(oidcOpts, oidc.WithScopes(am.ClaimsScopes...)) + } + // a bare min oidc.Request needed for the provider.AuthURL(...) call. We've intentionally not populated // things like Audiences, because this oidc.Request isn't cached and not intended for use in future legs // of the authen flow. diff --git a/internal/auth/oidc/service_start_auth_test.go b/internal/auth/oidc/service_start_auth_test.go index f7095549d..eccb302d9 100644 --- a/internal/auth/oidc/service_start_auth_test.go +++ b/internal/auth/oidc/service_start_auth_test.go @@ -6,7 +6,9 @@ import ( "net/http" "net/http/httptest" "net/url" + "sort" "strconv" + "strings" "testing" "time" @@ -50,6 +52,7 @@ func Test_StartAuth(t *testing.T) { WithApiUrl(TestConvertToUrls(t, testController.URL)[0]), WithSigningAlgs(Alg(tpAlg)), WithCertificates(tpCert...), + WithClaimsScopes("email", "profile"), ) testAuthMethodInactive := TestAuthMethod( @@ -172,13 +175,19 @@ func Test_StartAuth(t *testing.T) { authParams, err := url.ParseQuery(authUrl.RawQuery) require.NoError(err) assert.Equal(authParams["response_type"], []string{"code"}) - assert.Equal(authParams["scope"], []string{"openid"}) assert.Equal(authParams["response_type"], []string{"code"}) assert.Equal(authParams["redirect_uri"], []string{allowedRedirect}) assert.Equal(authParams["client_id"], []string{tt.authMethod.ClientId}) require.Equal(1, len(authParams["nonce"])) require.Equal(1, len(authParams["state"])) + require.Len(authParams["scope"], 1) + wantClaimsScopes := append(tt.authMethod.ClaimsScopes, DefaultClaimsScope) + gotScopes := strings.Split(authParams["scope"][0], " ") + sort.Strings(gotScopes) + sort.Strings(wantClaimsScopes) + assert.Equal(wantClaimsScopes, gotScopes) + if tt.authMethod.MaxAge != 0 { var expected int switch { diff --git a/internal/auth/oidc/service_test.go b/internal/auth/oidc/service_test.go index 8cdf7c901..243eb6a0b 100644 --- a/internal/auth/oidc/service_test.go +++ b/internal/auth/oidc/service_test.go @@ -5,7 +5,6 @@ import ( "testing" "time" - "github.com/golang/protobuf/ptypes" "github.com/hashicorp/boundary/internal/auth/oidc/request" "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/db/timestamp" @@ -17,6 +16,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" ) // Test_encryptState_decryptState are unit tests for both encryptState(...) and decryptState(...) @@ -31,10 +31,9 @@ func Test_encryptMessage_decryptMessage(t *testing.T) { testAuthMethod := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, ActivePrivateState, "alice-rp", "fido", WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), WithSigningAlgs(RS256)) now := time.Now() - createTime, err := ptypes.TimestampProto(now.Truncate(time.Second)) - require.NoError(t, err) - exp, err := ptypes.TimestampProto(now.Add(AttemptExpiration).Truncate(time.Second)) + createTime := timestamppb.New(now.Truncate(time.Second)) require.NoError(t, err) + exp := timestamppb.New(now.Add(AttemptExpiration).Truncate(time.Second)) tests := []struct { name string diff --git a/internal/auth/oidc/service_token_request_test.go b/internal/auth/oidc/service_token_request_test.go index 29e00d410..9efd4f314 100644 --- a/internal/auth/oidc/service_token_request_test.go +++ b/internal/auth/oidc/service_token_request_test.go @@ -7,7 +7,6 @@ import ( "time" "github.com/golang/protobuf/proto" - "github.com/golang/protobuf/ptypes" "github.com/hashicorp/boundary/internal/auth/oidc/request" "github.com/hashicorp/boundary/internal/authtoken" "github.com/hashicorp/boundary/internal/db" @@ -18,6 +17,7 @@ import ( "github.com/mr-tron/base58" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" ) func Test_TokenRequest(t *testing.T) { @@ -234,8 +234,7 @@ func Test_TokenRequest(t *testing.T) { atRepoFn: atRepoFn, authMethodId: testAuthMethod.PublicId, tokenRequest: func() string { - exp, err := ptypes.TimestampProto(time.Now().Add(AttemptExpiration).Truncate(time.Second)) - require.NoError(t, err) + exp := timestamppb.New(time.Now().Add(AttemptExpiration).Truncate(time.Second)) reqTk := request.Token{ ExpirationTime: ×tamp.Timestamp{Timestamp: exp}, } @@ -264,8 +263,7 @@ func Test_TokenRequest(t *testing.T) { atRepoFn: atRepoFn, authMethodId: testAuthMethod.PublicId, tokenRequest: func() string { - exp, err := ptypes.TimestampProto(time.Now().Add(AttemptExpiration).Truncate(time.Second)) - require.NoError(t, err) + exp := timestamppb.New(time.Now().Add(AttemptExpiration).Truncate(time.Second)) reqTk := request.Token{ RequestId: "not-a-valid-id", ExpirationTime: ×tamp.Timestamp{Timestamp: exp}, diff --git a/internal/auth/oidc/store/oidc.pb.go b/internal/auth/oidc/store/oidc.pb.go index e95edd415..b33b0de74 100644 --- a/internal/auth/oidc/store/oidc.pb.go +++ b/internal/auth/oidc/store/oidc.pb.go @@ -112,6 +112,11 @@ type AuthMethod struct { // as a complete set. // @inject_tag: `gorm:"-"` Certificates []string `protobuf:"bytes,190,rep,name=certificates,proto3" json:"certificates,omitempty" gorm:"-"` + // claims_scopes are the optional claims scopes a specific oidc auth method. + // These are Value Objects that will be stored as ClaimsScope messages, and + // are operatated on as a complete set. + // @inject_tag: `gorm:"-"` + ClaimsScopes []string `protobuf:"bytes,200,rep,name=claims_scopes,json=claimsScopes,proto3" json:"claims_scopes,omitempty" gorm:"-"` } func (x *AuthMethod) Reset() { @@ -293,6 +298,13 @@ func (x *AuthMethod) GetCertificates() []string { return nil } +func (x *AuthMethod) GetClaimsScopes() []string { + if x != nil { + return x.ClaimsScopes + } + return nil +} + // Account represents an OIDC account // the scope_id column is not included here as it is used only to ensure // data integrity in the database between iam users and auth methods. @@ -651,6 +663,78 @@ func (x *Certificate) GetCreateTime() *timestamp.Timestamp { return nil } +// ClaimsScope entries are optional OIDC scope values that are used to request +// claims, in addition to the default scope of "openid". +// +// see: https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims +type ClaimsScope struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // @inject_tag: `gorm:"primary_key"` + OidcMethodId string `protobuf:"bytes,10,opt,name=oidc_method_id,json=oidcMethodId,proto3" json:"oidc_method_id,omitempty" gorm:"primary_key"` + // scope is an OIDC scope + // @inject_tag: `gorm:"column:scope;primary_key"` + Scope string `protobuf:"bytes,20,opt,name=scope,proto3" json:"scope,omitempty" gorm:"column:scope;primary_key"` + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,30,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` +} + +func (x *ClaimsScope) Reset() { + *x = ClaimsScope{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ClaimsScope) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClaimsScope) ProtoMessage() {} + +func (x *ClaimsScope) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClaimsScope.ProtoReflect.Descriptor instead. +func (*ClaimsScope) Descriptor() ([]byte, []int) { + return file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDescGZIP(), []int{5} +} + +func (x *ClaimsScope) GetOidcMethodId() string { + if x != nil { + return x.OidcMethodId + } + return "" +} + +func (x *ClaimsScope) GetScope() string { + if x != nil { + return x.Scope + } + return "" +} + +func (x *ClaimsScope) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + var File_controller_storage_auth_oidc_store_v1_oidc_proto protoreflect.FileDescriptor var file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDesc = []byte{ @@ -667,7 +751,7 @@ var file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDesc = []byte{ 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xc9, 0x09, 0x0a, 0x0a, 0x41, 0x75, 0x74, 0x68, 0x4d, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x9d, 0x0a, 0x0a, 0x0a, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, @@ -744,68 +828,83 @@ var file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDesc = []byte{ 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x12, 0x17, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x69, 0x64, 0x70, 0x5f, 0x63, 0x61, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x73, 0x52, 0x0c, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, - 0x65, 0x73, 0x22, 0xcd, 0x03, 0x0a, 0x07, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1b, - 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, - 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, - 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, - 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, - 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, - 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x28, 0x20, - 0x01, 0x28, 0x09, 0x42, 0x10, 0xc2, 0xdd, 0x29, 0x0c, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x64, - 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x32, 0x20, 0x01, 0x28, 0x09, - 0x42, 0x1e, 0xc2, 0xdd, 0x29, 0x1a, 0x0a, 0x0b, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, - 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x3c, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, - 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x5f, - 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x46, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0c, 0x61, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, 0x16, 0x0a, - 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x18, 0x50, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x69, - 0x73, 0x73, 0x75, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x18, 0x5a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, - 0x1b, 0x0a, 0x09, 0x66, 0x75, 0x6c, 0x6c, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x64, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x08, 0x66, 0x75, 0x6c, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, - 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x6e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, - 0x69, 0x6c, 0x22, 0x91, 0x01, 0x0a, 0x0a, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x41, 0x6c, - 0x67, 0x12, 0x24, 0x0a, 0x0e, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, - 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6f, 0x69, 0x64, 0x63, 0x4d, - 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x67, 0x18, 0x14, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x61, 0x6c, 0x67, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, - 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, - 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, - 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x8f, 0x01, 0x0a, 0x08, 0x41, 0x75, 0x64, 0x43, 0x6c, - 0x61, 0x69, 0x6d, 0x12, 0x24, 0x0a, 0x0e, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x6d, 0x65, 0x74, 0x68, + 0x65, 0x73, 0x12, 0x52, 0x0a, 0x0d, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x5f, 0x73, 0x63, 0x6f, + 0x70, 0x65, 0x73, 0x18, 0xc8, 0x01, 0x20, 0x03, 0x28, 0x09, 0x42, 0x2c, 0xc2, 0xdd, 0x29, 0x28, + 0x0a, 0x0c, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x12, 0x18, + 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x63, 0x6c, 0x61, 0x69, 0x6d, + 0x73, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x52, 0x0c, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x73, + 0x53, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x22, 0xcd, 0x03, 0x0a, 0x07, 0x41, 0x63, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, + 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, + 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x14, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, + 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, + 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, + 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x28, 0x20, 0x01, 0x28, 0x09, 0x42, 0x10, 0xc2, 0xdd, 0x29, 0x0c, 0x0a, 0x04, 0x4e, + 0x61, 0x6d, 0x65, 0x12, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, + 0x40, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x32, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x1e, 0xc2, 0xdd, 0x29, 0x1a, 0x0a, 0x0b, 0x44, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x3c, 0x20, 0x01, + 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0e, 0x61, + 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x46, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, + 0x64, 0x12, 0x16, 0x0a, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x18, 0x50, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x18, 0x5a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x75, 0x6c, 0x6c, 0x5f, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x75, 0x6c, 0x6c, 0x4e, 0x61, 0x6d, 0x65, + 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x6e, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x22, 0x91, 0x01, 0x0a, 0x0a, 0x53, 0x69, 0x67, 0x6e, 0x69, + 0x6e, 0x67, 0x41, 0x6c, 0x67, 0x12, 0x24, 0x0a, 0x0e, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x6d, 0x65, + 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6f, + 0x69, 0x64, 0x63, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x61, + 0x6c, 0x67, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x61, 0x6c, 0x67, 0x12, 0x4b, 0x0a, + 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x1e, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, + 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, + 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x8f, 0x01, 0x0a, 0x08, 0x41, + 0x75, 0x64, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x12, 0x24, 0x0a, 0x0e, 0x6f, 0x69, 0x64, 0x63, 0x5f, + 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0c, 0x6f, 0x69, 0x64, 0x63, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, 0x10, 0x0a, + 0x03, 0x61, 0x75, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x61, 0x75, 0x64, 0x12, + 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x1e, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, + 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x94, 0x01, 0x0a, + 0x0b, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x24, 0x0a, 0x0e, + 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6f, 0x69, 0x64, 0x63, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x65, 0x72, 0x74, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x63, 0x65, 0x72, 0x74, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, + 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, + 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, + 0x69, 0x6d, 0x65, 0x22, 0x96, 0x01, 0x0a, 0x0b, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x53, 0x63, + 0x6f, 0x70, 0x65, 0x12, 0x24, 0x0a, 0x0e, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6f, 0x69, 0x64, - 0x63, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x75, 0x64, - 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x61, 0x75, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, - 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, - 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x94, 0x01, 0x0a, 0x0b, 0x43, 0x65, 0x72, - 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x24, 0x0a, 0x0e, 0x6f, 0x69, 0x64, 0x63, - 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0c, 0x6f, 0x69, 0x64, 0x63, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, 0x12, - 0x0a, 0x04, 0x63, 0x65, 0x72, 0x74, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x65, - 0x72, 0x74, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, - 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, - 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x42, - 0x3e, 0x5a, 0x3c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, - 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, - 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x6f, - 0x69, 0x64, 0x63, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x63, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x6f, + 0x70, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x12, + 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x1e, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, + 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x42, 0x3e, 0x5a, 0x3c, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, + 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x6f, 0x69, 0x64, 0x63, + 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -820,28 +919,30 @@ func file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDescGZIP() []byte return file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDescData } -var file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes = make([]protoimpl.MessageInfo, 6) var file_controller_storage_auth_oidc_store_v1_oidc_proto_goTypes = []interface{}{ (*AuthMethod)(nil), // 0: controller.storage.auth.oidc.store.v1.AuthMethod (*Account)(nil), // 1: controller.storage.auth.oidc.store.v1.Account (*SigningAlg)(nil), // 2: controller.storage.auth.oidc.store.v1.SigningAlg (*AudClaim)(nil), // 3: controller.storage.auth.oidc.store.v1.AudClaim (*Certificate)(nil), // 4: controller.storage.auth.oidc.store.v1.Certificate - (*timestamp.Timestamp)(nil), // 5: controller.storage.timestamp.v1.Timestamp + (*ClaimsScope)(nil), // 5: controller.storage.auth.oidc.store.v1.ClaimsScope + (*timestamp.Timestamp)(nil), // 6: controller.storage.timestamp.v1.Timestamp } var file_controller_storage_auth_oidc_store_v1_oidc_proto_depIdxs = []int32{ - 5, // 0: controller.storage.auth.oidc.store.v1.AuthMethod.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 1: controller.storage.auth.oidc.store.v1.AuthMethod.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 2: controller.storage.auth.oidc.store.v1.Account.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 3: controller.storage.auth.oidc.store.v1.Account.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 4: controller.storage.auth.oidc.store.v1.SigningAlg.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 5: controller.storage.auth.oidc.store.v1.AudClaim.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 5, // 6: controller.storage.auth.oidc.store.v1.Certificate.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 7, // [7:7] is the sub-list for method output_type - 7, // [7:7] is the sub-list for method input_type - 7, // [7:7] is the sub-list for extension type_name - 7, // [7:7] is the sub-list for extension extendee - 0, // [0:7] is the sub-list for field type_name + 6, // 0: controller.storage.auth.oidc.store.v1.AuthMethod.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 1: controller.storage.auth.oidc.store.v1.AuthMethod.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 2: controller.storage.auth.oidc.store.v1.Account.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 3: controller.storage.auth.oidc.store.v1.Account.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 4: controller.storage.auth.oidc.store.v1.SigningAlg.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 5: controller.storage.auth.oidc.store.v1.AudClaim.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 6: controller.storage.auth.oidc.store.v1.Certificate.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 6, // 7: controller.storage.auth.oidc.store.v1.ClaimsScope.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // [8:8] is the sub-list for method output_type + 8, // [8:8] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name } func init() { file_controller_storage_auth_oidc_store_v1_oidc_proto_init() } @@ -910,6 +1011,18 @@ func file_controller_storage_auth_oidc_store_v1_oidc_proto_init() { return nil } } + file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ClaimsScope); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -917,7 +1030,7 @@ func file_controller_storage_auth_oidc_store_v1_oidc_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDesc, NumEnums: 0, - NumMessages: 5, + NumMessages: 6, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/auth/oidc/testing.go b/internal/auth/oidc/testing.go index 10b88fea4..84e11c998 100644 --- a/internal/auth/oidc/testing.go +++ b/internal/auth/oidc/testing.go @@ -21,7 +21,6 @@ import ( "testing" "time" - "github.com/golang/protobuf/ptypes" "github.com/hashicorp/boundary/internal/auth/oidc/request" "github.com/hashicorp/boundary/internal/authtoken" "github.com/hashicorp/boundary/internal/db" @@ -32,6 +31,7 @@ import ( wrapping "github.com/hashicorp/go-kms-wrapping" "github.com/jinzhu/gorm" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" ) // TestAuthMethod creates a test oidc auth method. WithName, WithDescription, @@ -97,9 +97,20 @@ func TestAuthMethod( require.NoError(err) require.Equal(len(opts.withSigningAlgs), len(authMethod.SigningAlgs)) } + if len(opts.withClaimsScopes) > 0 { + newClaimsScopes := make([]interface{}, 0, len(opts.withClaimsScopes)) + for _, cs := range opts.withClaimsScopes { + s, err := NewClaimsScope(authMethod.PublicId, cs) + require.NoError(err) + newClaimsScopes = append(newClaimsScopes, s) + } + err := rw.CreateItems(ctx, newClaimsScopes) + require.NoError(err) + require.Equal(len(opts.withClaimsScopes), len(authMethod.ClaimsScopes)) + } authMethod.OperationalState = string(state) - rowsUpdated, err := rw.Update(ctx, authMethod, []string{"OperationalState"}, nil) + rowsUpdated, err := rw.Update(ctx, authMethod, []string{OperationalStateField}, nil) require.NoError(err) require.True(rowsUpdated == 0 || rowsUpdated == 1) @@ -126,6 +137,9 @@ func TestSortAuthMethods(t *testing.T, methods []*AuthMethod) { sort.Slice(am.Certificates, func(a, b int) bool { return am.Certificates[a] < am.Certificates[b] }) + sort.Slice(am.ClaimsScopes, func(a, b int) bool { + return am.ClaimsScopes[a] < am.ClaimsScopes[b] + }) } } @@ -263,10 +277,8 @@ func testState( require.NotNil(kms) now := time.Now() - createTime, err := ptypes.TimestampProto(now.Truncate(time.Second)) - require.NoError(err) - exp, err := ptypes.TimestampProto(now.Add(expIn).Truncate(time.Second)) - require.NoError(err) + createTime := timestamppb.New(now.Truncate(time.Second)) + exp := timestamppb.New(now.Add(expIn).Truncate(time.Second)) st := &request.State{ TokenRequestId: tokenRequestId, @@ -297,8 +309,7 @@ func TestTokenRequestId( require := require.New(t) now := time.Now() - exp, err := ptypes.TimestampProto(now.Add(expIn).Truncate(time.Second)) - require.NoError(err) + exp := timestamppb.New(now.Add(expIn).Truncate(time.Second)) reqTk := &request.Token{ RequestId: tokenPublicId, diff --git a/internal/cmd/commands/authmethodscmd/oidc_funcs.go b/internal/cmd/commands/authmethodscmd/oidc_funcs.go index d3cb9bcdf..43ad54535 100644 --- a/internal/cmd/commands/authmethodscmd/oidc_funcs.go +++ b/internal/cmd/commands/authmethodscmd/oidc_funcs.go @@ -26,6 +26,7 @@ type extraOidcCmdVars struct { flagSigningAlgorithms []string flagIdpCaCerts []string flagAllowedAudiences []string + flagClaimsScopes []string flagDisableDiscoveredConfigValidation bool flagDryRun bool } @@ -40,6 +41,7 @@ const ( apiUrlPrefixFlagName = "api-url-prefix" caCertFlagName = "idp-ca-cert" allowedAudienceFlagName = "allowed-audience" + claimsScopes = "claims-scopes" stateFlagName = "state" disableDiscoveredConfigValidationFlagName = "disable-discovered-config-validation" dryRunFlagName = "dry-run" @@ -56,6 +58,7 @@ func extraOidcActionsFlagsMapFuncImpl() map[string][]string { apiUrlPrefixFlagName, caCertFlagName, allowedAudienceFlagName, + claimsScopes, }, "change-state": { idFlagName, @@ -120,6 +123,12 @@ func extraOidcFlagsFuncImpl(c *OidcCommand, set *base.FlagSets, _ *base.FlagSet) Target: &c.flagAllowedAudiences, Usage: `The acceptable audience ("aud") claim. May be specified multiple times.`, }) + case claimsScopes: + f.StringSliceVar(&base.StringSliceVar{ + Name: claimsScopes, + Target: &c.flagClaimsScopes, + Usage: `The optional claims scope requested. May be specified multiple times.`, + }) case stateFlagName: f.StringVar(&base.StringVar{ Name: stateFlagName, @@ -242,6 +251,13 @@ func extraOidcFlagHandlingFuncImpl(c *OidcCommand, f *base.FlagSets, opts *[]aut default: *opts = append(*opts, authmethods.WithOidcAuthMethodAllowedAudiences(c.flagAllowedAudiences)) } + switch { + case len(c.flagClaimsScopes) == 0: + case len(c.flagClaimsScopes) == 1 && c.flagClaimsScopes[0] == "null": + *opts = append(*opts, authmethods.DefaultOidcAuthMethodClaimsScopes()) + default: + *opts = append(*opts, authmethods.WithOidcAuthMethodClaimsScopes(c.flagClaimsScopes)) + } if c.flagDisableDiscoveredConfigValidation { *opts = append(*opts, authmethods.WithOidcAuthMethodDisableDiscoveredConfigValidation(c.flagDisableDiscoveredConfigValidation)) } diff --git a/internal/db/schema/migrations/postgres/5/01_oidc.up.sql b/internal/db/schema/migrations/postgres/5/01_oidc.up.sql new file mode 100644 index 000000000..7b372490a --- /dev/null +++ b/internal/db/schema/migrations/postgres/5/01_oidc.up.sql @@ -0,0 +1,85 @@ +begin; + + +-- auth_oidc_scope entries are the optional scopes for a specific oidc auth +-- method. There can be 0 or more for each parent oidc auth method. If an auth +-- method has any scopes, they will be added to provider requests along with the +-- default of "openid". +create table auth_oidc_scope ( + create_time wt_timestamp, + oidc_method_id wt_public_id + constraint auth_oidc_method_fkey + references auth_oidc_method(public_id) + on delete cascade + on update cascade, + scope text not null + constraint scope_must_not_be_empty + check(length(trim(scope)) > 0) + constraint scope_must_be_less_than_1024_chars + check(length(trim(scope)) < 1024) + constraint scope_must_not_be_openid -- the default scope is not allowed, since it's redundant + check(lower(trim(scope)) != 'openid'), + primary key(oidc_method_id, scope) +); +comment on table auth_oidc_scope is +'auth_oidc_scope entries are the optional scopes for a specific oidc auth method. There can be 0 or more for each parent oidc auth method. If an auth method has any scopes, they will be added to provider requests along with the openid default.'; + + +create trigger + default_create_time_column +before +insert on auth_oidc_scope + for each row execute procedure default_create_time(); + + +-- we will drop the oidc_auth_method_with_value_obj view, so we can recreate it +-- and add the oidc claim's scopes to the returned set. +drop view oidc_auth_method_with_value_obj; + +-- oidc_auth_method_with_value_obj is useful for reading an oidc auth method +-- with its associated value objects (algs, auds, certs) as columns +-- with | delimited values. The use of the postgres string_agg(...) to +-- aggregate the value objects into a column works because we are only pulling +-- in one column from the associated tables and that value is part of the +-- primary key and unique. This view will make things like recursive listing of +-- oidc auth methods fairly straightforward to implement for the oidc repo. +-- The view also includes an is_primary_auth_method bool +create view oidc_auth_method_with_value_obj as +select + case when s.primary_auth_method_id is not null then + true + else false end + as is_primary_auth_method, + am.public_id, + am.scope_id, + am.name, + am.description, + am.create_time, + am.update_time, + am.version, + am.state, + am.api_url, + am.disable_discovered_config_validation, + am.issuer, + am.client_id, + am.client_secret, + am.client_secret_hmac, + am.key_id, + am.max_age, + -- the string_agg(..) column will be null if there are no associated value objects + string_agg(distinct alg.signing_alg_name, '|') as algs, + string_agg(distinct aud.aud_claim, '|') as auds, + string_agg(distinct cert.certificate, '|') as certs, + string_agg(distinct cs.scope, '|') as claims_scopes +from + auth_oidc_method am + left outer join iam_scope s on am.public_id = s.primary_auth_method_id + left outer join auth_oidc_signing_alg alg on am.public_id = alg.oidc_method_id + left outer join auth_oidc_aud_claim aud on am.public_id = aud.oidc_method_id + left outer join auth_oidc_certificate cert on am.public_id = cert.oidc_method_id + left outer join auth_oidc_scope cs on am.public_id = cs.oidc_method_id +group by am.public_id, is_primary_auth_method; -- there can be only one public_id + is_primary_auth_method, so group by isn't a problem. +comment on view oidc_auth_method_with_value_obj is +'oidc auth method with its associated value objects (algs, auds, certs, scopes) as columns with | delimited values'; + +commit; diff --git a/internal/db/schema/postgres_migration.gen.go b/internal/db/schema/postgres_migration.gen.go index 532715c4c..82c7ce708 100644 --- a/internal/db/schema/postgres_migration.gen.go +++ b/internal/db/schema/postgres_migration.gen.go @@ -4,7 +4,7 @@ package schema func init() { migrationStates["postgres"] = migrationState{ - binarySchemaVersion: 4001, + binarySchemaVersion: 5001, upMigrations: map[int][]byte{ 1: []byte(` create domain wt_public_id as text @@ -6005,6 +6005,88 @@ select from iam_user u left outer join iam_acct_info i on u.public_id = i.iam_user_id; +`), + 5001: []byte(` +-- auth_oidc_scope entries are the optional scopes for a specific oidc auth +-- method. There can be 0 or more for each parent oidc auth method. If an auth +-- method has any scopes, they will be added to provider requests along with the +-- default of "openid". +create table auth_oidc_scope ( + create_time wt_timestamp, + oidc_method_id wt_public_id + constraint auth_oidc_method_fkey + references auth_oidc_method(public_id) + on delete cascade + on update cascade, + scope text not null + constraint scope_must_not_be_empty + check(length(trim(scope)) > 0) + constraint scope_must_be_less_than_1024_chars + check(length(trim(scope)) < 1024) + constraint scope_must_not_be_openid -- the default scope is not allowed, since it's redundant + check(lower(trim(scope)) != 'openid'), + primary key(oidc_method_id, scope) +); +comment on table auth_oidc_scope is +'auth_oidc_scope entries are the optional scopes for a specific oidc auth method. There can be 0 or more for each parent oidc auth method. If an auth method has any scopes, they will be added to provider requests along with the openid default.'; + + +create trigger + default_create_time_column +before +insert on auth_oidc_scope + for each row execute procedure default_create_time(); + + +-- we will drop the oidc_auth_method_with_value_obj view, so we can recreate it +-- and add the oidc claim's scopes to the returned set. +drop view oidc_auth_method_with_value_obj; + +-- oidc_auth_method_with_value_obj is useful for reading an oidc auth method +-- with its associated value objects (algs, auds, certs) as columns +-- with | delimited values. The use of the postgres string_agg(...) to +-- aggregate the value objects into a column works because we are only pulling +-- in one column from the associated tables and that value is part of the +-- primary key and unique. This view will make things like recursive listing of +-- oidc auth methods fairly straightforward to implement for the oidc repo. +-- The view also includes an is_primary_auth_method bool +create view oidc_auth_method_with_value_obj as +select + case when s.primary_auth_method_id is not null then + true + else false end + as is_primary_auth_method, + am.public_id, + am.scope_id, + am.name, + am.description, + am.create_time, + am.update_time, + am.version, + am.state, + am.api_url, + am.disable_discovered_config_validation, + am.issuer, + am.client_id, + am.client_secret, + am.client_secret_hmac, + am.key_id, + am.max_age, + -- the string_agg(..) column will be null if there are no associated value objects + string_agg(distinct alg.signing_alg_name, '|') as algs, + string_agg(distinct aud.aud_claim, '|') as auds, + string_agg(distinct cert.certificate, '|') as certs, + string_agg(distinct cs.scope, '|') as claims_scopes +from + auth_oidc_method am + left outer join iam_scope s on am.public_id = s.primary_auth_method_id + left outer join auth_oidc_signing_alg alg on am.public_id = alg.oidc_method_id + left outer join auth_oidc_aud_claim aud on am.public_id = aud.oidc_method_id + left outer join auth_oidc_certificate cert on am.public_id = cert.oidc_method_id + left outer join auth_oidc_scope cs on am.public_id = cs.oidc_method_id +group by am.public_id, is_primary_auth_method; -- there can be only one public_id + is_primary_auth_method, so group by isn't a problem. +comment on view oidc_auth_method_with_value_obj is +'oidc auth method with its associated value objects (algs, auds, certs, scopes) as columns with | delimited values'; `), }, } diff --git a/internal/gen/controller/api/resources/authmethods/auth_method.pb.go b/internal/gen/controller/api/resources/authmethods/auth_method.pb.go index 213d49fe2..71926c221 100644 --- a/internal/gen/controller/api/resources/authmethods/auth_method.pb.go +++ b/internal/gen/controller/api/resources/authmethods/auth_method.pb.go @@ -282,6 +282,9 @@ type OidcAuthMethodAttributes struct { IdpCaCerts []string `protobuf:"bytes,100,rep,name=idp_ca_certs,proto3" json:"idp_ca_certs,omitempty"` // Allowed audience claims for this auth method. AllowedAudiences []string `protobuf:"bytes,110,rep,name=allowed_audiences,proto3" json:"allowed_audiences,omitempty"` + // Optional claims scopes that will be requested during authentication. + // see: https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims + ClaimsScopes []string `protobuf:"bytes,112,rep,name=claims_scopes,proto3" json:"claims_scopes,omitempty"` // If the Authorization Server's discovered configuration contains values // that do not match the configuration set on this auth method, this can be // set to force the local configuration to override the discovered values. @@ -409,6 +412,13 @@ func (x *OidcAuthMethodAttributes) GetAllowedAudiences() []string { return nil } +func (x *OidcAuthMethodAttributes) GetClaimsScopes() []string { + if x != nil { + return x.ClaimsScopes + } + return nil +} + func (x *OidcAuthMethodAttributes) GetDisableDiscoveredConfigValidation() bool { if x != nil { return x.DisableDiscoveredConfigValidation @@ -805,7 +815,7 @@ var file_controller_api_resources_authmethods_v1_auth_method_proto_rawDesc = []b 0x6e, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x12, 0x11, 0x4d, 0x69, 0x6e, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x52, 0x13, 0x6d, 0x69, 0x6e, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, - 0x72, 0x64, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x22, 0xa3, 0x08, 0x0a, 0x18, 0x4f, 0x69, + 0x72, 0x64, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x22, 0xfb, 0x08, 0x0a, 0x18, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x59, 0x0a, 0x06, @@ -864,53 +874,59 @@ var file_controller_api_resources_authmethods_v1_auth_method_proto_rawDesc = []b 0x75, 0x74, 0x65, 0x73, 0x2e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x61, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x09, 0x41, 0x75, 0x64, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x52, 0x11, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x61, 0x75, 0x64, 0x69, 0x65, - 0x6e, 0x63, 0x65, 0x73, 0x12, 0x58, 0x0a, 0x24, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, - 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x78, 0x20, 0x01, - 0x28, 0x08, 0x42, 0x04, 0xa0, 0xda, 0x29, 0x01, 0x52, 0x24, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, - 0x65, 0x5f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1f, - 0x0a, 0x07, 0x64, 0x72, 0x79, 0x5f, 0x72, 0x75, 0x6e, 0x18, 0x82, 0x01, 0x20, 0x01, 0x28, 0x08, - 0x42, 0x04, 0xa0, 0xda, 0x29, 0x01, 0x52, 0x07, 0x64, 0x72, 0x79, 0x5f, 0x72, 0x75, 0x6e, 0x22, - 0x61, 0x0a, 0x27, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, - 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, - 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x75, - 0x74, 0x68, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x75, - 0x74, 0x68, 0x5f, 0x75, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, - 0x69, 0x64, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, - 0x69, 0x64, 0x22, 0xb7, 0x01, 0x0a, 0x29, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, - 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, - 0x65, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x63, 0x6f, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x14, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, - 0x72, 0x6f, 0x72, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, - 0x12, 0x2c, 0x0a, 0x11, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x28, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x65, 0x72, 0x72, - 0x6f, 0x72, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1c, - 0x0a, 0x09, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x32, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x09, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x75, 0x72, 0x69, 0x22, 0x5c, 0x0a, 0x2a, - 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, - 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, - 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x66, 0x69, - 0x6e, 0x61, 0x6c, 0x5f, 0x72, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x5f, 0x75, 0x72, 0x6c, - 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x72, 0x65, - 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x5f, 0x75, 0x72, 0x6c, 0x22, 0x44, 0x0a, 0x26, 0x4f, 0x69, - 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, - 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x69, 0x64, - 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x69, 0x64, - 0x22, 0x41, 0x0a, 0x27, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, - 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x54, 0x6f, - 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x42, 0x5d, 0x5a, 0x5b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, - 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, - 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, - 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, - 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x6d, - 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x3b, 0x61, 0x75, 0x74, 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, - 0x64, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6e, 0x63, 0x65, 0x73, 0x12, 0x56, 0x0a, 0x0d, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x5f, 0x73, + 0x63, 0x6f, 0x70, 0x65, 0x73, 0x18, 0x70, 0x20, 0x03, 0x28, 0x09, 0x42, 0x30, 0xa0, 0xda, 0x29, + 0x01, 0xc2, 0xdd, 0x29, 0x28, 0x0a, 0x18, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x73, 0x2e, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x12, + 0x0c, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x52, 0x0d, 0x63, + 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x12, 0x58, 0x0a, 0x24, + 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, + 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x78, 0x20, 0x01, 0x28, 0x08, 0x42, 0x04, 0xa0, 0xda, 0x29, 0x01, + 0x52, 0x24, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, + 0x65, 0x72, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x76, 0x61, 0x6c, 0x69, + 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a, 0x07, 0x64, 0x72, 0x79, 0x5f, 0x72, 0x75, + 0x6e, 0x18, 0x82, 0x01, 0x20, 0x01, 0x28, 0x08, 0x42, 0x04, 0xa0, 0xda, 0x29, 0x01, 0x52, 0x07, + 0x64, 0x72, 0x79, 0x5f, 0x72, 0x75, 0x6e, 0x22, 0x61, 0x0a, 0x27, 0x4f, 0x69, 0x64, 0x63, 0x41, + 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, + 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x75, 0x72, 0x6c, 0x12, 0x1a, + 0x0a, 0x08, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x69, 0x64, 0x22, 0xb7, 0x01, 0x0a, 0x29, 0x4f, + 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, + 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, + 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, + 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, + 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, + 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x1e, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x2c, 0x0a, 0x11, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x28, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x11, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, + 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, + 0x75, 0x72, 0x69, 0x18, 0x32, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x65, 0x72, 0x72, 0x6f, 0x72, + 0x5f, 0x75, 0x72, 0x69, 0x22, 0x5c, 0x0a, 0x2a, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, + 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, + 0x74, 0x65, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x72, 0x65, 0x64, 0x69, + 0x72, 0x65, 0x63, 0x74, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, + 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x72, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x5f, 0x75, + 0x72, 0x6c, 0x22, 0x44, 0x0a, 0x26, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, + 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, + 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, + 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x69, 0x64, 0x22, 0x41, 0x0a, 0x27, 0x4f, 0x69, 0x64, 0x63, + 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, + 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x42, 0x5d, 0x5a, 0x5b, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, + 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x73, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x3b, 0x61, + 0x75, 0x74, 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( diff --git a/internal/proto/local/controller/api/resources/authmethods/v1/auth_method.proto b/internal/proto/local/controller/api/resources/authmethods/v1/auth_method.proto index 31c43cd8c..c96da96b1 100644 --- a/internal/proto/local/controller/api/resources/authmethods/v1/auth_method.proto +++ b/internal/proto/local/controller/api/resources/authmethods/v1/auth_method.proto @@ -12,162 +12,175 @@ import "controller/custom_options/v1/options.proto"; // AuthMethod contains all fields related to an Auth Method resource message AuthMethod { - // Output only. The ID of the Auth Method. - string id = 10; + // Output only. The ID of the Auth Method. + string id = 10; - // The ID of the Scope of which this Auth Method is a part. - string scope_id = 20 [json_name="scope_id"]; + // The ID of the Scope of which this Auth Method is a part. + string scope_id = 20 [json_name = "scope_id"]; - // Output only. Scope information for this Auth method. - resources.scopes.v1.ScopeInfo scope = 30; + // Output only. Scope information for this Auth method. + resources.scopes.v1.ScopeInfo scope = 30; - // Optional name for identification purposes. - google.protobuf.StringValue name = 40 [(custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"name" that: "Name"}]; + // Optional name for identification purposes. + google.protobuf.StringValue name = 40 [(custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = { this: "name" that: "Name" }]; - // Optional user-set description for identification purposes. - google.protobuf.StringValue description = 50 [(custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"description" that: "Description"}]; + // Optional user-set description for identification purposes. + google.protobuf.StringValue description = 50 [(custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = { this: "description" that: "Description" }]; - // Output only. The time this resource was created. - google.protobuf.Timestamp created_time = 60 [json_name="created_time"]; + // Output only. The time this resource was created. + google.protobuf.Timestamp created_time = 60 [json_name = "created_time"]; - // Output only. The time this resource was last updated. - google.protobuf.Timestamp updated_time = 70 [json_name="updated_time"]; + // Output only. The time this resource was last updated. + google.protobuf.Timestamp updated_time = 70 [json_name = "updated_time"]; - // Version is used in mutation requests, after the initial creation, to ensure this resource has not changed. - // The mutation will fail if the version does not match the latest known good version. - uint32 version = 80; + // Version is used in mutation requests, after the initial creation, to ensure this resource has not changed. + // The mutation will fail if the version does not match the latest known good version. + uint32 version = 80; - // The Auth Method type. - string type = 90; + // The Auth Method type. + string type = 90; - // The attributes that are applicable for the specific Auth Method type. - google.protobuf.Struct attributes = 100 [(custom_options.v1.generate_sdk_option) = true]; + // The attributes that are applicable for the specific Auth Method type. + google.protobuf.Struct attributes = 100 [(custom_options.v1.generate_sdk_option) = true]; - // Output only. Whether this auth method is the primary auth method for it's scope. - // To change this value update the primary_auth_method_id field on the scope. - bool is_primary = 110 [json_name = "is_primary"]; + // Output only. Whether this auth method is the primary auth method for it's scope. + // To change this value update the primary_auth_method_id field on the scope. + bool is_primary = 110 [json_name = "is_primary"]; - // Output only. The available actions on this resource for this user. - repeated string authorized_actions = 300 [json_name="authorized_actions"]; + // Output only. The available actions on this resource for this user. + repeated string authorized_actions = 300 [json_name = "authorized_actions"]; - // Output only. The authorized actions for the scope's collections. - map authorized_collection_actions = 310 [json_name="authorized_collection_actions"]; + // Output only. The authorized actions for the scope's collections. + map authorized_collection_actions = 310 [json_name = "authorized_collection_actions"]; } // The attributes of a password typed auth method. message PasswordAuthMethodAttributes { - // The minimum length allowed for user names for Accounts in this Auth Method. - uint32 min_login_name_length = 10 [json_name="min_login_name_length", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"attributes.min_login_name_length" that:"MinLoginNameLength"}]; + // The minimum length allowed for user names for Accounts in this Auth Method. + uint32 min_login_name_length = 10 + [json_name = "min_login_name_length", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = { this: "attributes.min_login_name_length" that: "MinLoginNameLength" }]; - // The minimum length allowed for passwords for Accounts in this Auth Method. - uint32 min_password_length = 20 [json_name="min_password_length", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"attributes.min_password_length" that:"MinPasswordLength"}]; + // The minimum length allowed for passwords for Accounts in this Auth Method. + uint32 min_password_length = 20 + [json_name = "min_password_length", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = { this: "attributes.min_password_length" that: "MinPasswordLength" }]; } // The attributes of an OIDC typed auth method. message OidcAuthMethodAttributes { - // Output only. The state of the auth method. Will be "inactive", - // "active-private", or "active-public". - string state = 10 [json_name="state"]; + // Output only. The state of the auth method. Will be "inactive", + // "active-private", or "active-public". + string state = 10 [json_name = "state"]; - // The issuer URL. Boundary expects only the schema, host, and port and will - // strip off ".well-known/openid-configuration" if present. This will be - // used for configuration discovery as well as for validation of the "iss" - // claim. - google.protobuf.StringValue issuer = 20 [json_name="issuer", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"attributes.issuer" that:"Issuer"}]; + // The issuer URL. Boundary expects only the schema, host, and port and will + // strip off ".well-known/openid-configuration" if present. This will be + // used for configuration discovery as well as for validation of the "iss" + // claim. + google.protobuf.StringValue issuer = 20 [json_name = "issuer", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = { this: "attributes.issuer" that: "Issuer" }]; - // An OAuth 2.0 Client Identifier valid at the Authorization Server. - google.protobuf.StringValue client_id = 30 [json_name="client_id", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"attributes.client_id" that:"ClientId"}]; + // An OAuth 2.0 Client Identifier valid at the Authorization Server. + google.protobuf.StringValue client_id = 30 + [json_name = "client_id", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = { this: "attributes.client_id" that: "ClientId" }]; - // Input only. The client's secret. - google.protobuf.StringValue client_secret = 40 [json_name="client_secret", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"attributes.client_secret" that:"ClientSecret"}]; + // Input only. The client's secret. + google.protobuf.StringValue client_secret = 40 + [json_name = "client_secret", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = { this: "attributes.client_secret" that: "ClientSecret" }]; - // Output only. The HMAC'd value of the clients secret to indicate whether - // the client secret has changed. - string client_secret_hmac = 50 [json_name="client_secret_hmac"]; + // Output only. The HMAC'd value of the clients secret to indicate whether + // the client secret has changed. + string client_secret_hmac = 50 [json_name = "client_secret_hmac"]; - // The OIDC "max_age" parameter sent to the Authorization Server indicating - // a maximum acceptable time in seconds since the user's last authentication - // before requiring the user to reauthenticate. 0 indicates an immediate - // need to reauthenticate. - google.protobuf.UInt32Value max_age = 60 [json_name="max_age", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"attributes.max_age" that:"MaxAge"}]; + // The OIDC "max_age" parameter sent to the Authorization Server indicating + // a maximum acceptable time in seconds since the user's last authentication + // before requiring the user to reauthenticate. 0 indicates an immediate + // need to reauthenticate. + google.protobuf.UInt32Value max_age = 60 [json_name = "max_age", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = { this: "attributes.max_age" that: "MaxAge" }]; - // The signing algorithms allowed for the auth method. - repeated string signing_algorithms = 70 [json_name="signing_algorithms", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"attributes.signing_algorithms" that:"SigningAlgs"}]; + // The signing algorithms allowed for the auth method. + repeated string signing_algorithms = 70 + [json_name = "signing_algorithms", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = { this: "attributes.signing_algorithms" that: "SigningAlgs" }]; - // The prefix that should be used for any URLs needed during the - // authentication flow. This includes the callback URL, the token retrieval - // URL, and the redirection URL used by the OIDC Authorization Server. - google.protobuf.StringValue api_url_prefix = 80 [json_name="api_url_prefix", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"attributes.api_url_prefix" that:"ApiUrl"}]; + // The prefix that should be used for any URLs needed during the + // authentication flow. This includes the callback URL, the token retrieval + // URL, and the redirection URL used by the OIDC Authorization Server. + google.protobuf.StringValue api_url_prefix = 80 + [json_name = "api_url_prefix", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = { this: "attributes.api_url_prefix" that: "ApiUrl" }]; - // Output only. The callback URL that should be configured on the - // Authorization Server to use during the authentication flow. - string callback_url = 90 [json_name="callback_url"]; + // Output only. The callback URL that should be configured on the + // Authorization Server to use during the authentication flow. + string callback_url = 90 [json_name = "callback_url"]; - // Optional PEM-encoded X.509 CA certificates that can be used as trust anchors - // when connecting to an OIDC provider. - repeated string idp_ca_certs = 100 [json_name="idp_ca_certs", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"attributes.idp_ca_certs" that:"Certificates"}]; + // Optional PEM-encoded X.509 CA certificates that can be used as trust anchors + // when connecting to an OIDC provider. + repeated string idp_ca_certs = 100 + [json_name = "idp_ca_certs", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = { this: "attributes.idp_ca_certs" that: "Certificates" }]; - // Allowed audience claims for this auth method. - repeated string allowed_audiences = 110 [json_name="allowed_audiences", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"attributes.allowed_audiences" that:"AudClaims"}]; + // Allowed audience claims for this auth method. + repeated string allowed_audiences = 110 + [json_name = "allowed_audiences", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = { this: "attributes.allowed_audiences" that: "AudClaims" }]; - // If the Authorization Server's discovered configuration contains values - // that do not match the configuration set on this auth method, this can be - // set to force the local configuration to override the discovered values. - // This is useful in the case of slow-changing or incorrect values announced - // by the Authorization Server. NOTE: For safety, not providing this value - // will be treated the same as "false"; that is, every time the method is - // updated or the state is changed, this value must be set to "true" or it - // will be disabled. - bool disable_discovered_config_validation = 120 [json_name="disable_discovered_config_validation", (custom_options.v1.generate_sdk_option) = true]; + // Optional claims scopes that will be requested during authentication. + // see: https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims + repeated string claims_scopes = 112 + [json_name = "claims_scopes", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = { this: "attributes.claims_scopes" that: "ClaimsScopes" }]; - // dry_run, when set on an update request, indicates that the changes should - // not be persisted. Boundary will still perform the normal checks to confirm - // the auth method is complete and validated against the discovered config. - // This value will also be set on the returned resource when set in the request - // along with the updated fields applied to the resource (but not persisted) as - // a result of the update request. - bool dry_run = 130 [json_name="dry_run", (custom_options.v1.generate_sdk_option) = true]; + // If the Authorization Server's discovered configuration contains values + // that do not match the configuration set on this auth method, this can be + // set to force the local configuration to override the discovered values. + // This is useful in the case of slow-changing or incorrect values announced + // by the Authorization Server. NOTE: For safety, not providing this value + // will be treated the same as "false"; that is, every time the method is + // updated or the state is changed, this value must be set to "true" or it + // will be disabled. + bool disable_discovered_config_validation = 120 [json_name = "disable_discovered_config_validation", (custom_options.v1.generate_sdk_option) = true]; + + // dry_run, when set on an update request, indicates that the changes should + // not be persisted. Boundary will still perform the normal checks to confirm + // the auth method is complete and validated against the discovered config. + // This value will also be set on the returned resource when set in the request + // along with the updated fields applied to the resource (but not persisted) as + // a result of the update request. + bool dry_run = 130 [json_name = "dry_run", (custom_options.v1.generate_sdk_option) = true]; } // The structure of the OIDC authenticate start response, in the JSON object message OidcAuthMethodAuthenticateStartResponse { - // The returned authentication URL - string auth_url = 10 [json_name="auth_url"]; + // The returned authentication URL + string auth_url = 10 [json_name = "auth_url"]; - // The returned token ID - string token_id = 30 [json_name="token_id"]; + // The returned token ID + string token_id = 30 [json_name = "token_id"]; } // The structure of OIDC callback request parameters message OidcAuthMethodAuthenticateCallbackRequest { - // The returned code - string code = 10 [json_name="code"]; + // The returned code + string code = 10 [json_name = "code"]; - // The returned state - string state = 20 [json_name="state"]; + // The returned state + string state = 20 [json_name = "state"]; - // Error parameters, if they are returned - string error = 30 [json_name="error"]; - string error_description = 40 [json_name="error_description"]; - string error_uri = 50 [json_name="error_uri"]; + // Error parameters, if they are returned + string error = 30 [json_name = "error"]; + string error_description = 40 [json_name = "error_description"]; + string error_uri = 50 [json_name = "error_uri"]; } // The structure of OIDC callback response parameters message OidcAuthMethodAuthenticateCallbackResponse { - // The final redirection URL - string final_redirect_url = 10 [json_name="final_redirect_url"]; + // The final redirection URL + string final_redirect_url = 10 [json_name = "final_redirect_url"]; } // The structure of OIDC token request parameters message OidcAuthMethodAuthenticateTokenRequest { - // The ID of the pending token - string token_id = 10 [json_name="token_id"]; + // The ID of the pending token + string token_id = 10 [json_name = "token_id"]; } // Internal only: the structure of a token response if it _does not_ contain a // token. message OidcAuthMethodAuthenticateTokenResponse { - // The status. This will always be "unknown". It will never be forwarded to - // the consumer. - string status = 10; + // The status. This will always be "unknown". It will never be forwarded to + // the consumer. + string status = 10; } \ No newline at end of file diff --git a/internal/proto/local/controller/storage/auth/oidc/store/v1/oidc.proto b/internal/proto/local/controller/storage/auth/oidc/store/v1/oidc.proto index eb02104e6..642295a40 100644 --- a/internal/proto/local/controller/storage/auth/oidc/store/v1/oidc.proto +++ b/internal/proto/local/controller/storage/auth/oidc/store/v1/oidc.proto @@ -111,6 +111,12 @@ message AuthMethod { // as a complete set. // @inject_tag: `gorm:"-"` repeated string certificates = 190 [(custom_options.v1.mask_mapping) = { this: "Certificates" that: "attributes.idp_ca_certs" }]; + + // claims_scopes are the optional claims scopes a specific oidc auth method. + // These are Value Objects that will be stored as ClaimsScope messages, and + // are operatated on as a complete set. + // @inject_tag: `gorm:"-"` + repeated string claims_scopes = 200 [(custom_options.v1.mask_mapping) = { this: "ClaimsScopes" that: "attributes.claims_scopes" }]; } // Account represents an OIDC account @@ -198,6 +204,23 @@ message Certificate { // @inject_tag: `gorm:"column:certificate;primary_key"` string cert = 20; + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 30; +} + +// ClaimsScope entries are optional OIDC scope values that are used to request +// claims, in addition to the default scope of "openid". +// +// see: https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims +message ClaimsScope { + // @inject_tag: `gorm:"primary_key"` + string oidc_method_id = 10; + + // scope is an OIDC scope + // @inject_tag: `gorm:"column:scope;primary_key"` + string scope = 20; + // The create_time is set by the database. // @inject_tag: `gorm:"default:current_timestamp"` timestamp.v1.Timestamp create_time = 30; diff --git a/internal/servers/controller/handlers/authmethods/authmethod_service.go b/internal/servers/controller/handlers/authmethods/authmethod_service.go index 95723aa3c..62043a209 100644 --- a/internal/servers/controller/handlers/authmethods/authmethod_service.go +++ b/internal/servers/controller/handlers/authmethods/authmethod_service.go @@ -677,6 +677,7 @@ func toAuthMethodProto(in auth.AuthMethod, opt ...handlers.Option) (*pb.AuthMeth State: i.GetOperationalState(), SigningAlgorithms: i.GetSigningAlgs(), AllowedAudiences: i.GetAudClaims(), + ClaimsScopes: i.GetClaimsScopes(), } if i.DisableDiscoveredConfigValidation { attrs.DisableDiscoveredConfigValidation = true @@ -805,6 +806,14 @@ func validateCreateRequest(req *pbs.CreateAuthMethodRequest) error { badFields[caCertsField] = fmt.Sprintf("Cannot parse CA certificates. %v", err.Error()) } } + if len(attrs.GetClaimsScopes()) > 0 { + for _, cs := range attrs.GetClaimsScopes() { + if cs == oidc.DefaultClaimsScope { + badFields[claimsScopesField] = fmt.Sprintf("%s is the default scope and cannot be added as optional %q", oidc.DefaultClaimsScope, cs) + break + } + } + } } default: badFields[typeField] = fmt.Sprintf("This is a required field and must be %q.", auth.PasswordSubtype.String()) @@ -905,6 +914,14 @@ func validateUpdateRequest(req *pbs.UpdateAuthMethodRequest) error { badFields[caCertsField] = fmt.Sprintf("Cannot parse CA certificates. %v", err.Error()) } } + if len(attrs.GetClaimsScopes()) > 0 { + for _, cs := range attrs.GetClaimsScopes() { + if cs == oidc.DefaultClaimsScope { + badFields[claimsScopesField] = fmt.Sprintf("%s is the default scope and cannot be added as optional %q", oidc.DefaultClaimsScope, cs) + break + } + } + } default: badFields["id"] = "Incorrectly formatted identifier." } diff --git a/internal/servers/controller/handlers/authmethods/authmethod_service_test.go b/internal/servers/controller/handlers/authmethods/authmethod_service_test.go index 0a3465faa..1057d3a35 100644 --- a/internal/servers/controller/handlers/authmethods/authmethod_service_test.go +++ b/internal/servers/controller/handlers/authmethods/authmethod_service_test.go @@ -568,6 +568,10 @@ func TestCreate(t *testing.T) { lv, _ := structpb.NewList([]interface{}{"foo", "bar"}) return structpb.NewListValue(lv) }(), + "claims_scopes": func() *structpb.Value { + lv, _ := structpb.NewList([]interface{}{"email", "profile"}) + return structpb.NewListValue(lv) + }(), }}, }}, idPrefix: oidc.AuthMethodPrefix + "_", @@ -592,6 +596,10 @@ func TestCreate(t *testing.T) { lv, _ := structpb.NewList([]interface{}{"foo", "bar"}) return structpb.NewListValue(lv) }(), + "claims_scopes": func() *structpb.Value { + lv, _ := structpb.NewList([]interface{}{"email", "profile"}) + return structpb.NewListValue(lv) + }(), }}, AuthorizedActions: oidcAuthorizedActions, AuthorizedCollectionActions: authorizedCollectionActions, @@ -917,6 +925,24 @@ func TestCreate(t *testing.T) { }}, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, + { + name: "OIDC AuthMethod cant specify default claims scopes of openid", + req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "api_url_prefix": structpb.NewStringValue("https://api.com"), + "issuer": structpb.NewStringValue("https://example.discovery.url:4821/.well-known/openid-configuration/"), + "client_id": structpb.NewStringValue("someclientid"), + "client_secret": structpb.NewStringValue("secret"), + "claims_scopes": func() *structpb.Value { + lv, _ := structpb.NewList([]interface{}{"openid"}) + return structpb.NewListValue(lv) + }(), + }}, + }}, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { diff --git a/internal/servers/controller/handlers/authmethods/oidc.go b/internal/servers/controller/handlers/authmethods/oidc.go index 6c96a94a8..1b081a45e 100644 --- a/internal/servers/controller/handlers/authmethods/oidc.go +++ b/internal/servers/controller/handlers/authmethods/oidc.go @@ -41,6 +41,7 @@ const ( disableDiscoveredConfigValidationField = "attributes.disable_discovered_config_validation" roundtripPayloadAttributesField = "attributes.roundtrip_payload" codeField = "attributes.code" + claimsScopesField = "attributes.claims_scopes" ) var oidcMaskManager handlers.MaskManager @@ -458,6 +459,10 @@ func toStorageOidcAuthMethod(scopeId string, in *pb.AuthMethod) (out *oidc.AuthM opts = append(opts, oidc.WithCertificates(certs...)) } + if len(attrs.GetClaimsScopes()) > 0 { + opts = append(opts, oidc.WithClaimsScopes(attrs.GetClaimsScopes()...)) + } + u, err := oidc.NewAuthMethod(scopeId, clientId, clientSecret, opts...) if err != nil { return nil, false, false, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build auth method: %v.", err) diff --git a/internal/servers/controller/handlers/authmethods/oidc_test.go b/internal/servers/controller/handlers/authmethods/oidc_test.go index 30d2591b9..6308511cf 100644 --- a/internal/servers/controller/handlers/authmethods/oidc_test.go +++ b/internal/servers/controller/handlers/authmethods/oidc_test.go @@ -865,6 +865,45 @@ func TestUpdate_OIDC(t *testing.T) { }, }, }, + { + name: "Change Claims Scopes", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.claims_scopes"}, + }, + Item: &pb.AuthMethod{ + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "claims_scopes": func() *structpb.Value { + lv, _ := structpb.NewList([]interface{}{"email", "profile"}) + return structpb.NewListValue(lv) + }(), + }, + }, + }, + }, + res: &pbs.UpdateAuthMethodResponse{ + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "default"}, + Description: &wrapperspb.StringValue{Value: "default"}, + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{ + Fields: func() map[string]*structpb.Value { + f := defaultReadAttributeFields() + f["claims_scopes"] = func() *structpb.Value { + lv, _ := structpb.NewList([]interface{}{"email", "profile"}) + return structpb.NewListValue(lv) + }() + return f + }(), + }, + Scope: defaultScopeInfo, + AuthorizedActions: oidcAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, { name: "Unset Issuer Is Incomplete", req: &pbs.UpdateAuthMethodRequest{