diff --git a/components/gitpod-cli/cmd/idp-gcloud-token.go b/components/gitpod-cli/cmd/idp-gcloud-token.go index 90dcc17a210e5c..155d73d907b544 100644 --- a/components/gitpod-cli/cmd/idp-gcloud-token.go +++ b/components/gitpod-cli/cmd/idp-gcloud-token.go @@ -43,7 +43,7 @@ var idpGCloudTokenCmd = &cobra.Command{ } }() - tkn, err := idpToken(ctx, idpGCloudTokenOpts.Audience) + tkn, err := idpToken(ctx, idpGCloudTokenOpts.Audience, "") if err != nil { return err } diff --git a/components/gitpod-cli/cmd/idp-login-aws.go b/components/gitpod-cli/cmd/idp-login-aws.go index 473911d28322ce..6ce9bd8752c9f8 100644 --- a/components/gitpod-cli/cmd/idp-login-aws.go +++ b/components/gitpod-cli/cmd/idp-login-aws.go @@ -43,7 +43,7 @@ var idpLoginAwsCmd = &cobra.Command{ ctx, cancel := context.WithTimeout(cmd.Context(), 5*time.Second) defer cancel() - tkn, err := idpToken(ctx, idpLoginAwsOpts.Audience) + tkn, err := idpToken(ctx, idpLoginAwsOpts.Audience, idpLoginOpts.Scope) if err != nil { return err } diff --git a/components/gitpod-cli/cmd/idp-login-vault.go b/components/gitpod-cli/cmd/idp-login-vault.go index fabc90f60c5e1a..11a883e953ae10 100644 --- a/components/gitpod-cli/cmd/idp-login-vault.go +++ b/components/gitpod-cli/cmd/idp-login-vault.go @@ -33,7 +33,7 @@ var idpLoginVaultCmd = &cobra.Command{ ctx, cancel := context.WithTimeout(cmd.Context(), 5*time.Second) defer cancel() - tkn, err := idpToken(ctx, idpLoginVaultOpts.Audience) + tkn, err := idpToken(ctx, idpLoginVaultOpts.Audience, idpLoginOpts.Scope) if err != nil { return err } diff --git a/components/gitpod-cli/cmd/idp-login.go b/components/gitpod-cli/cmd/idp-login.go index ed37231776ed91..1582e19c94f0ea 100644 --- a/components/gitpod-cli/cmd/idp-login.go +++ b/components/gitpod-cli/cmd/idp-login.go @@ -8,6 +8,10 @@ import ( "github.com/spf13/cobra" ) +var idpLoginOpts struct { + Scope string +} + var idpLoginCmd = &cobra.Command{ Use: "login", Short: "Login to a service for which trust has been established", @@ -15,4 +19,6 @@ var idpLoginCmd = &cobra.Command{ func init() { idpCmd.AddCommand(idpLoginCmd) + + idpLoginCmd.PersistentFlags().StringVar(&idpLoginOpts.Scope, "scope", "", "scopes string of the ID token") } diff --git a/components/gitpod-cli/cmd/idp-token.go b/components/gitpod-cli/cmd/idp-token.go index d1e4ed2ce22264..8326f8eaeaf7ea 100644 --- a/components/gitpod-cli/cmd/idp-token.go +++ b/components/gitpod-cli/cmd/idp-token.go @@ -26,6 +26,7 @@ import ( var idpTokenOpts struct { Audience []string Decode bool + Scope string } var idpTokenCmd = &cobra.Command{ @@ -37,7 +38,7 @@ var idpTokenCmd = &cobra.Command{ ctx, cancel := context.WithTimeout(cmd.Context(), 5*time.Second) defer cancel() - tkn, err := idpToken(ctx, idpTokenOpts.Audience) + tkn, err := idpToken(ctx, idpTokenOpts.Audience, idpTokenOpts.Scope) token, _, err := jwt.NewParser().ParseUnverified(tkn, jwt.MapClaims{}) if err != nil { @@ -66,7 +67,7 @@ var idpTokenCmd = &cobra.Command{ }, } -func idpToken(ctx context.Context, audience []string) (idToken string, err error) { +func idpToken(ctx context.Context, audience []string, scope string) (idToken string, err error) { wsInfo, err := gitpod.GetWSInfo(ctx) if err != nil { return "", err @@ -95,6 +96,7 @@ func idpToken(ctx context.Context, audience []string) (idToken string, err error Msg: &v1.GetIDTokenRequest{ Audience: audience, WorkspaceId: wsInfo.WorkspaceId, + Scope: scope, }, }) if err != nil { @@ -110,4 +112,5 @@ func init() { _ = idpTokenCmd.MarkFlagRequired("audience") idpTokenCmd.Flags().BoolVar(&idpTokenOpts.Decode, "decode", false, "decode token to JSON") + idpTokenCmd.Flags().StringVar(&idpTokenOpts.Scope, "scope", "", "scopes string of the ID token") } diff --git a/components/public-api-server/pkg/apiv1/identityprovider.go b/components/public-api-server/pkg/apiv1/identityprovider.go index 93ac16a3cfb2b9..e666e37865e97a 100644 --- a/components/public-api-server/pkg/apiv1/identityprovider.go +++ b/components/public-api-server/pkg/apiv1/identityprovider.go @@ -96,6 +96,10 @@ func (srv *IdentityProviderService) GetIDToken(ctx context.Context, req *connect userInfo.AppendClaims("context", workspace.Workspace.ContextURL) userInfo.AppendClaims("workspace_id", workspaceID) + if req.Msg.GetScope() != "" { + userInfo.AppendClaims("scope", req.Msg.GetScope()) + } + if workspace.Workspace.Context != nil && workspace.Workspace.Context.Repository != nil && workspace.Workspace.Context.Repository.CloneURL != "" { userInfo.AppendClaims("repository", workspace.Workspace.Context.Repository.CloneURL) } diff --git a/components/public-api-server/pkg/apiv1/identityprovider_test.go b/components/public-api-server/pkg/apiv1/identityprovider_test.go index 159c61f7848928..8955ed5adc563b 100644 --- a/components/public-api-server/pkg/apiv1/identityprovider_test.go +++ b/components/public-api-server/pkg/apiv1/identityprovider_test.go @@ -205,6 +205,56 @@ func TestGetIDToken(t *testing.T) { Error: connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Must have at least one audience entry")).Error(), }, }, + { + Name: "include scope", + TokenSource: func(t *testing.T) IDTokenSource { + return functionIDTokenSource(func(ctx context.Context, org string, audience []string, userInfo oidc.UserInfo) (string, error) { + require.Equal(t, "correct@gitpod.io", userInfo.GetEmail()) + require.True(t, userInfo.IsEmailVerified()) + require.Equal(t, "foo", userInfo.GetClaim("scope")) + + return "foobar", nil + }) + }, + ServerSetup: func(ma *protocol.MockAPIInterface) { + ma.EXPECT().GetIDToken(gomock.Any()).MinTimes(1).Return(nil) + ma.EXPECT().GetWorkspace(gomock.Any(), workspaceID).MinTimes(1).Return( + &protocol.WorkspaceInfo{ + Workspace: &protocol.Workspace{ + ContextURL: "https://github.com/gitpod-io/gitpod", + Context: &protocol.WorkspaceContext{ + Repository: &protocol.Repository{ + CloneURL: "https://github.com/gitpod-io/gitpod.git", + }, + }, + }, + }, + nil, + ) + ma.EXPECT().GetLoggedInUser(gomock.Any()).Return( + &protocol.User{ + Name: "foobar", + Identities: []*protocol.Identity{ + nil, + {Deleted: true, PrimaryEmail: "nonsense@gitpod.io"}, + {Deleted: false, PrimaryEmail: "correct@gitpod.io"}, + }, + OrganizationId: "test", + }, + nil, + ) + }, + Request: &v1.GetIDTokenRequest{ + WorkspaceId: workspaceID, + Audience: []string{"some.audience.com"}, + Scope: "foo", + }, + Expectation: Expectation{ + Response: &v1.GetIDTokenResponse{ + Token: "foobar", + }, + }, + }, { Name: "token source error", TokenSource: func(t *testing.T) IDTokenSource { diff --git a/components/public-api-server/pkg/identityprovider/idp_test.go b/components/public-api-server/pkg/identityprovider/idp_test.go index 9ebb537a626600..946afe31556d46 100644 --- a/components/public-api-server/pkg/identityprovider/idp_test.go +++ b/components/public-api-server/pkg/identityprovider/idp_test.go @@ -146,6 +146,28 @@ func TestIDToken(t *testing.T) { }, }, }, + { + Name: "with custom claims", + Audience: []string{"some.audience.com"}, + UserInfo: func() oidc.UserInfo { + userInfo := oidc.NewUserInfo() + userInfo.AppendClaims("scope", "foobar") + return userInfo + }(), + Expectation: Expectation{ + Token: &jwt.Token{ + Method: &jwt.SigningMethodRSA{Name: "RS256", Hash: crypto.SHA256}, + Header: map[string]interface{}{"alg": string(jose.RS256)}, + Claims: jwt.MapClaims{ + "aud": []any{string("some.audience.com")}, + "azp": string("some.audience.com"), + "iss": string("https://api.gitpod.io/idp"), + "scope": "foobar", + }, + Valid: true, + }, + }, + }, } for _, test := range tests { diff --git a/components/public-api/gitpod/experimental/v1/identityprovider.proto b/components/public-api/gitpod/experimental/v1/identityprovider.proto index 379a913f226727..81bf7cc637f6a3 100644 --- a/components/public-api/gitpod/experimental/v1/identityprovider.proto +++ b/components/public-api/gitpod/experimental/v1/identityprovider.proto @@ -12,6 +12,7 @@ service IdentityProviderService { message GetIDTokenRequest { string workspace_id = 1; repeated string audience = 2; + string scope = 3; } message GetIDTokenResponse { diff --git a/components/public-api/go/experimental/v1/identityprovider.pb.go b/components/public-api/go/experimental/v1/identityprovider.pb.go index bf0a8e652e12d5..bd7155382b8bb9 100644 --- a/components/public-api/go/experimental/v1/identityprovider.pb.go +++ b/components/public-api/go/experimental/v1/identityprovider.pb.go @@ -31,6 +31,7 @@ type GetIDTokenRequest struct { WorkspaceId string `protobuf:"bytes,1,opt,name=workspace_id,json=workspaceId,proto3" json:"workspace_id,omitempty"` Audience []string `protobuf:"bytes,2,rep,name=audience,proto3" json:"audience,omitempty"` + Scope string `protobuf:"bytes,3,opt,name=scope,proto3" json:"scope,omitempty"` } func (x *GetIDTokenRequest) Reset() { @@ -79,6 +80,13 @@ func (x *GetIDTokenRequest) GetAudience() []string { return nil } +func (x *GetIDTokenRequest) GetScope() string { + if x != nil { + return x.Scope + } + return "" +} + type GetIDTokenResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -133,28 +141,29 @@ var file_gitpod_experimental_v1_identityprovider_proto_rawDesc = []byte{ 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x2f, 0x76, 0x31, 0x2f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x16, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, - 0x6e, 0x74, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x22, 0x52, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x49, 0x44, + 0x6e, 0x74, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x22, 0x68, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, 0x03, 0x28, - 0x09, 0x52, 0x08, 0x61, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x22, 0x2a, 0x0a, 0x12, 0x47, - 0x65, 0x74, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x32, 0x80, 0x01, 0x0a, 0x17, 0x49, 0x64, 0x65, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x12, 0x65, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, - 0x6e, 0x12, 0x29, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x65, 0x78, 0x70, 0x65, 0x72, - 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x49, 0x44, - 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x67, - 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, - 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x46, 0x5a, 0x44, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2d, - 0x69, 0x6f, 0x2f, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2f, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, - 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x2d, 0x61, 0x70, 0x69, 0x2f, - 0x67, 0x6f, 0x2f, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x2f, - 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x09, 0x52, 0x08, 0x61, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, + 0x63, 0x6f, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x70, + 0x65, 0x22, 0x2a, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x32, 0x80, 0x01, + 0x0a, 0x17, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, + 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x65, 0x0a, 0x0a, 0x47, 0x65, 0x74, + 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x29, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, + 0x2e, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x2e, 0x76, 0x31, + 0x2e, 0x47, 0x65, 0x74, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x65, 0x78, 0x70, 0x65, + 0x72, 0x69, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x49, + 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x42, 0x46, 0x5a, 0x44, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, + 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2d, 0x69, 0x6f, 0x2f, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2f, + 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x70, 0x75, 0x62, 0x6c, 0x69, + 0x63, 0x2d, 0x61, 0x70, 0x69, 0x2f, 0x67, 0x6f, 0x2f, 0x65, 0x78, 0x70, 0x65, 0x72, 0x69, 0x6d, + 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/components/public-api/typescript/src/gitpod/experimental/v1/identityprovider_pb.ts b/components/public-api/typescript/src/gitpod/experimental/v1/identityprovider_pb.ts index c11ed2f385f0dd..6860d089f059f4 100644 --- a/components/public-api/typescript/src/gitpod/experimental/v1/identityprovider_pb.ts +++ b/components/public-api/typescript/src/gitpod/experimental/v1/identityprovider_pb.ts @@ -26,6 +26,11 @@ export class GetIDTokenRequest extends Message { */ audience: string[] = []; + /** + * @generated from field: string scope = 3; + */ + scope = ""; + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); @@ -36,6 +41,7 @@ export class GetIDTokenRequest extends Message { static readonly fields: FieldList = proto3.util.newFieldList(() => [ { no: 1, name: "workspace_id", kind: "scalar", T: 9 /* ScalarType.STRING */ }, { no: 2, name: "audience", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, + { no: 3, name: "scope", kind: "scalar", T: 9 /* ScalarType.STRING */ }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): GetIDTokenRequest {