diff --git a/Documentation/connectors/saml.md b/Documentation/connectors/saml.md index c9af0995e6..be29d394f7 100644 --- a/Documentation/connectors/saml.md +++ b/Documentation/connectors/saml.md @@ -6,8 +6,6 @@ The SAML provider allows authentication through the SAML 2.0 HTTP POST binding. The connector uses the value of the `NameID` element as the user's unique identifier which dex assumes is both unique and never changes. Use the `nameIDPolicyFormat` to ensure this is set to a value which satisfies these requirements. -Unlike some clients which will process unprompted AuthnResponses, dex must send the initial AuthnRequest and validates the response's InResponseTo value. - ## Caveats __The connector doesn't support refresh tokens__ since the SAML 2.0 protocol doesn't provide a way to requery a provider without interaction. If the "offline_access" scope is requested, it will be ignored. @@ -111,3 +109,44 @@ connectors: emailAttr: email groupsAttr: groups ``` + +## Identity Provider Initiated Login + +The SAML connector can support the identity provider (IdP) initiated flow with some special configuration. This flow allows a SAML user to initiate the login from a portal on their provider without needing to redirect back to the provider for further authentication. + +Because this flow does not follow the normal OAuth flow (App -> Auth Provider -> App), dex requires some configuration on the client to know how to handle an unprompted AuthnResponse. For each client that you would like to support SAML's IdP initiated flow, you must configure the `samlInitiated` section. + +As a static client: + +```yaml +staticClients: +- id: example-app + name: "Example App" + secret: ZXhhbXBsZS1hcHAtc2VjcmV0 + redirectURIs: + - http://127.0.0.1:5555/callback + samlInitiated: + redirecURI: http://127.0.0.1:5555/callback + scopes: ["openid", "profile", "email"] +``` + +Or via the API: + +```golang +client := &api.Client{ + Id: "example-app", + Name: "Example App", + Secret: "ZXhhbXBsZS1hcHAtc2VjcmV0", + RedirectUris: []string{"http://127.0.0.1:5555/callback"}, + SamlInitiated: &api.SamlInitiatedConfig{ + RedirectURI: "http://127.0.0.1:5555/callback", + Scopes: []string{"openid", "profile", "email"} + }, +} +``` + +The scopes above are optional. If they are not provided the default set `openid profile email groups` will be used. However, without `samlInitiated.redirecURI` dex will reject any IdP initiated login for that client. + +When configuring an application on your SAML provider, you must provide the `client_id` as the Default Relay Sate. This is how dex will match the AuthnResponse to the client, decide what claims to build, and where to redirect the user after handling the response. + +After handling the AuthnResponse dex will redirect back to your application with a `code` and an empty `state`. Your application must be willing to accept the empty `state` and redeem the `code` for user tokens. The implicit flow is not supported since there is no way of generating a verifiable `nonce` in this setup. diff --git a/api/api.pb.go b/api/api.pb.go index 5bac4e9514..b71c4d2fb8 100644 --- a/api/api.pb.go +++ b/api/api.pb.go @@ -26,16 +26,17 @@ const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package // Client represents an OAuth2 client. type Client struct { - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Secret string `protobuf:"bytes,2,opt,name=secret,proto3" json:"secret,omitempty"` - RedirectUris []string `protobuf:"bytes,3,rep,name=redirect_uris,json=redirectUris,proto3" json:"redirect_uris,omitempty"` - TrustedPeers []string `protobuf:"bytes,4,rep,name=trusted_peers,json=trustedPeers,proto3" json:"trusted_peers,omitempty"` - Public bool `protobuf:"varint,5,opt,name=public,proto3" json:"public,omitempty"` - Name string `protobuf:"bytes,6,opt,name=name,proto3" json:"name,omitempty"` - LogoUrl string `protobuf:"bytes,7,opt,name=logo_url,json=logoUrl,proto3" json:"logo_url,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Secret string `protobuf:"bytes,2,opt,name=secret,proto3" json:"secret,omitempty"` + RedirectUris []string `protobuf:"bytes,3,rep,name=redirect_uris,json=redirectUris,proto3" json:"redirect_uris,omitempty"` + TrustedPeers []string `protobuf:"bytes,4,rep,name=trusted_peers,json=trustedPeers,proto3" json:"trusted_peers,omitempty"` + Public bool `protobuf:"varint,5,opt,name=public,proto3" json:"public,omitempty"` + Name string `protobuf:"bytes,6,opt,name=name,proto3" json:"name,omitempty"` + LogoUrl string `protobuf:"bytes,7,opt,name=logo_url,json=logoUrl,proto3" json:"logo_url,omitempty"` + SamlInitiated *SamlInitiatedConfig `protobuf:"bytes,8,opt,name=saml_initiated,json=samlInitiated,proto3" json:"saml_initiated,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` } func (m *Client) Reset() { *m = Client{} } @@ -112,6 +113,61 @@ func (m *Client) GetLogoUrl() string { return "" } +func (m *Client) GetSamlInitiated() *SamlInitiatedConfig { + if m != nil { + return m.SamlInitiated + } + return nil +} + +// SamlInitiatedConfig provides config options for SAML initiated login on a client +type SamlInitiatedConfig struct { + RedirectUri string `protobuf:"bytes,1,opt,name=redirect_uri,json=redirectUri,proto3" json:"redirect_uri,omitempty"` + Scopes []string `protobuf:"bytes,2,rep,name=scopes,proto3" json:"scopes,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *SamlInitiatedConfig) Reset() { *m = SamlInitiatedConfig{} } +func (m *SamlInitiatedConfig) String() string { return proto.CompactTextString(m) } +func (*SamlInitiatedConfig) ProtoMessage() {} +func (*SamlInitiatedConfig) Descriptor() ([]byte, []int) { + return fileDescriptor_1b40cafcd4234784, []int{1} +} + +func (m *SamlInitiatedConfig) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_SamlInitiatedConfig.Unmarshal(m, b) +} +func (m *SamlInitiatedConfig) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_SamlInitiatedConfig.Marshal(b, m, deterministic) +} +func (m *SamlInitiatedConfig) XXX_Merge(src proto.Message) { + xxx_messageInfo_SamlInitiatedConfig.Merge(m, src) +} +func (m *SamlInitiatedConfig) XXX_Size() int { + return xxx_messageInfo_SamlInitiatedConfig.Size(m) +} +func (m *SamlInitiatedConfig) XXX_DiscardUnknown() { + xxx_messageInfo_SamlInitiatedConfig.DiscardUnknown(m) +} + +var xxx_messageInfo_SamlInitiatedConfig proto.InternalMessageInfo + +func (m *SamlInitiatedConfig) GetRedirectUri() string { + if m != nil { + return m.RedirectUri + } + return "" +} + +func (m *SamlInitiatedConfig) GetScopes() []string { + if m != nil { + return m.Scopes + } + return nil +} + // CreateClientReq is a request to make a client. type CreateClientReq struct { Client *Client `protobuf:"bytes,1,opt,name=client,proto3" json:"client,omitempty"` @@ -124,7 +180,7 @@ func (m *CreateClientReq) Reset() { *m = CreateClientReq{} } func (m *CreateClientReq) String() string { return proto.CompactTextString(m) } func (*CreateClientReq) ProtoMessage() {} func (*CreateClientReq) Descriptor() ([]byte, []int) { - return fileDescriptor_1b40cafcd4234784, []int{1} + return fileDescriptor_1b40cafcd4234784, []int{2} } func (m *CreateClientReq) XXX_Unmarshal(b []byte) error { @@ -165,7 +221,7 @@ func (m *CreateClientResp) Reset() { *m = CreateClientResp{} } func (m *CreateClientResp) String() string { return proto.CompactTextString(m) } func (*CreateClientResp) ProtoMessage() {} func (*CreateClientResp) Descriptor() ([]byte, []int) { - return fileDescriptor_1b40cafcd4234784, []int{2} + return fileDescriptor_1b40cafcd4234784, []int{3} } func (m *CreateClientResp) XXX_Unmarshal(b []byte) error { @@ -213,7 +269,7 @@ func (m *DeleteClientReq) Reset() { *m = DeleteClientReq{} } func (m *DeleteClientReq) String() string { return proto.CompactTextString(m) } func (*DeleteClientReq) ProtoMessage() {} func (*DeleteClientReq) Descriptor() ([]byte, []int) { - return fileDescriptor_1b40cafcd4234784, []int{3} + return fileDescriptor_1b40cafcd4234784, []int{4} } func (m *DeleteClientReq) XXX_Unmarshal(b []byte) error { @@ -253,7 +309,7 @@ func (m *DeleteClientResp) Reset() { *m = DeleteClientResp{} } func (m *DeleteClientResp) String() string { return proto.CompactTextString(m) } func (*DeleteClientResp) ProtoMessage() {} func (*DeleteClientResp) Descriptor() ([]byte, []int) { - return fileDescriptor_1b40cafcd4234784, []int{4} + return fileDescriptor_1b40cafcd4234784, []int{5} } func (m *DeleteClientResp) XXX_Unmarshal(b []byte) error { @@ -283,21 +339,22 @@ func (m *DeleteClientResp) GetNotFound() bool { // UpdateClientReq is a request to update an exisitng client. type UpdateClientReq struct { - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - RedirectUris []string `protobuf:"bytes,2,rep,name=redirect_uris,json=redirectUris,proto3" json:"redirect_uris,omitempty"` - TrustedPeers []string `protobuf:"bytes,3,rep,name=trusted_peers,json=trustedPeers,proto3" json:"trusted_peers,omitempty"` - Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"` - LogoUrl string `protobuf:"bytes,5,opt,name=logo_url,json=logoUrl,proto3" json:"logo_url,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + RedirectUris []string `protobuf:"bytes,2,rep,name=redirect_uris,json=redirectUris,proto3" json:"redirect_uris,omitempty"` + TrustedPeers []string `protobuf:"bytes,3,rep,name=trusted_peers,json=trustedPeers,proto3" json:"trusted_peers,omitempty"` + Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"` + LogoUrl string `protobuf:"bytes,5,opt,name=logo_url,json=logoUrl,proto3" json:"logo_url,omitempty"` + SamlInitiated *SamlInitiatedConfig `protobuf:"bytes,6,opt,name=saml_initiated,json=samlInitiated,proto3" json:"saml_initiated,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` } func (m *UpdateClientReq) Reset() { *m = UpdateClientReq{} } func (m *UpdateClientReq) String() string { return proto.CompactTextString(m) } func (*UpdateClientReq) ProtoMessage() {} func (*UpdateClientReq) Descriptor() ([]byte, []int) { - return fileDescriptor_1b40cafcd4234784, []int{5} + return fileDescriptor_1b40cafcd4234784, []int{6} } func (m *UpdateClientReq) XXX_Unmarshal(b []byte) error { @@ -353,6 +410,13 @@ func (m *UpdateClientReq) GetLogoUrl() string { return "" } +func (m *UpdateClientReq) GetSamlInitiated() *SamlInitiatedConfig { + if m != nil { + return m.SamlInitiated + } + return nil +} + // UpdateClientResp returns the reponse form updating a client. type UpdateClientResp struct { NotFound bool `protobuf:"varint,1,opt,name=not_found,json=notFound,proto3" json:"not_found,omitempty"` @@ -365,7 +429,7 @@ func (m *UpdateClientResp) Reset() { *m = UpdateClientResp{} } func (m *UpdateClientResp) String() string { return proto.CompactTextString(m) } func (*UpdateClientResp) ProtoMessage() {} func (*UpdateClientResp) Descriptor() ([]byte, []int) { - return fileDescriptor_1b40cafcd4234784, []int{6} + return fileDescriptor_1b40cafcd4234784, []int{7} } func (m *UpdateClientResp) XXX_Unmarshal(b []byte) error { @@ -409,7 +473,7 @@ func (m *Password) Reset() { *m = Password{} } func (m *Password) String() string { return proto.CompactTextString(m) } func (*Password) ProtoMessage() {} func (*Password) Descriptor() ([]byte, []int) { - return fileDescriptor_1b40cafcd4234784, []int{7} + return fileDescriptor_1b40cafcd4234784, []int{8} } func (m *Password) XXX_Unmarshal(b []byte) error { @@ -470,7 +534,7 @@ func (m *CreatePasswordReq) Reset() { *m = CreatePasswordReq{} } func (m *CreatePasswordReq) String() string { return proto.CompactTextString(m) } func (*CreatePasswordReq) ProtoMessage() {} func (*CreatePasswordReq) Descriptor() ([]byte, []int) { - return fileDescriptor_1b40cafcd4234784, []int{8} + return fileDescriptor_1b40cafcd4234784, []int{9} } func (m *CreatePasswordReq) XXX_Unmarshal(b []byte) error { @@ -510,7 +574,7 @@ func (m *CreatePasswordResp) Reset() { *m = CreatePasswordResp{} } func (m *CreatePasswordResp) String() string { return proto.CompactTextString(m) } func (*CreatePasswordResp) ProtoMessage() {} func (*CreatePasswordResp) Descriptor() ([]byte, []int) { - return fileDescriptor_1b40cafcd4234784, []int{9} + return fileDescriptor_1b40cafcd4234784, []int{10} } func (m *CreatePasswordResp) XXX_Unmarshal(b []byte) error { @@ -553,7 +617,7 @@ func (m *UpdatePasswordReq) Reset() { *m = UpdatePasswordReq{} } func (m *UpdatePasswordReq) String() string { return proto.CompactTextString(m) } func (*UpdatePasswordReq) ProtoMessage() {} func (*UpdatePasswordReq) Descriptor() ([]byte, []int) { - return fileDescriptor_1b40cafcd4234784, []int{10} + return fileDescriptor_1b40cafcd4234784, []int{11} } func (m *UpdatePasswordReq) XXX_Unmarshal(b []byte) error { @@ -607,7 +671,7 @@ func (m *UpdatePasswordResp) Reset() { *m = UpdatePasswordResp{} } func (m *UpdatePasswordResp) String() string { return proto.CompactTextString(m) } func (*UpdatePasswordResp) ProtoMessage() {} func (*UpdatePasswordResp) Descriptor() ([]byte, []int) { - return fileDescriptor_1b40cafcd4234784, []int{11} + return fileDescriptor_1b40cafcd4234784, []int{12} } func (m *UpdatePasswordResp) XXX_Unmarshal(b []byte) error { @@ -647,7 +711,7 @@ func (m *DeletePasswordReq) Reset() { *m = DeletePasswordReq{} } func (m *DeletePasswordReq) String() string { return proto.CompactTextString(m) } func (*DeletePasswordReq) ProtoMessage() {} func (*DeletePasswordReq) Descriptor() ([]byte, []int) { - return fileDescriptor_1b40cafcd4234784, []int{12} + return fileDescriptor_1b40cafcd4234784, []int{13} } func (m *DeletePasswordReq) XXX_Unmarshal(b []byte) error { @@ -687,7 +751,7 @@ func (m *DeletePasswordResp) Reset() { *m = DeletePasswordResp{} } func (m *DeletePasswordResp) String() string { return proto.CompactTextString(m) } func (*DeletePasswordResp) ProtoMessage() {} func (*DeletePasswordResp) Descriptor() ([]byte, []int) { - return fileDescriptor_1b40cafcd4234784, []int{13} + return fileDescriptor_1b40cafcd4234784, []int{14} } func (m *DeletePasswordResp) XXX_Unmarshal(b []byte) error { @@ -726,7 +790,7 @@ func (m *ListPasswordReq) Reset() { *m = ListPasswordReq{} } func (m *ListPasswordReq) String() string { return proto.CompactTextString(m) } func (*ListPasswordReq) ProtoMessage() {} func (*ListPasswordReq) Descriptor() ([]byte, []int) { - return fileDescriptor_1b40cafcd4234784, []int{14} + return fileDescriptor_1b40cafcd4234784, []int{15} } func (m *ListPasswordReq) XXX_Unmarshal(b []byte) error { @@ -759,7 +823,7 @@ func (m *ListPasswordResp) Reset() { *m = ListPasswordResp{} } func (m *ListPasswordResp) String() string { return proto.CompactTextString(m) } func (*ListPasswordResp) ProtoMessage() {} func (*ListPasswordResp) Descriptor() ([]byte, []int) { - return fileDescriptor_1b40cafcd4234784, []int{15} + return fileDescriptor_1b40cafcd4234784, []int{16} } func (m *ListPasswordResp) XXX_Unmarshal(b []byte) error { @@ -798,7 +862,7 @@ func (m *VersionReq) Reset() { *m = VersionReq{} } func (m *VersionReq) String() string { return proto.CompactTextString(m) } func (*VersionReq) ProtoMessage() {} func (*VersionReq) Descriptor() ([]byte, []int) { - return fileDescriptor_1b40cafcd4234784, []int{16} + return fileDescriptor_1b40cafcd4234784, []int{17} } func (m *VersionReq) XXX_Unmarshal(b []byte) error { @@ -835,7 +899,7 @@ func (m *VersionResp) Reset() { *m = VersionResp{} } func (m *VersionResp) String() string { return proto.CompactTextString(m) } func (*VersionResp) ProtoMessage() {} func (*VersionResp) Descriptor() ([]byte, []int) { - return fileDescriptor_1b40cafcd4234784, []int{17} + return fileDescriptor_1b40cafcd4234784, []int{18} } func (m *VersionResp) XXX_Unmarshal(b []byte) error { @@ -886,7 +950,7 @@ func (m *RefreshTokenRef) Reset() { *m = RefreshTokenRef{} } func (m *RefreshTokenRef) String() string { return proto.CompactTextString(m) } func (*RefreshTokenRef) ProtoMessage() {} func (*RefreshTokenRef) Descriptor() ([]byte, []int) { - return fileDescriptor_1b40cafcd4234784, []int{18} + return fileDescriptor_1b40cafcd4234784, []int{19} } func (m *RefreshTokenRef) XXX_Unmarshal(b []byte) error { @@ -948,7 +1012,7 @@ func (m *ListRefreshReq) Reset() { *m = ListRefreshReq{} } func (m *ListRefreshReq) String() string { return proto.CompactTextString(m) } func (*ListRefreshReq) ProtoMessage() {} func (*ListRefreshReq) Descriptor() ([]byte, []int) { - return fileDescriptor_1b40cafcd4234784, []int{19} + return fileDescriptor_1b40cafcd4234784, []int{20} } func (m *ListRefreshReq) XXX_Unmarshal(b []byte) error { @@ -988,7 +1052,7 @@ func (m *ListRefreshResp) Reset() { *m = ListRefreshResp{} } func (m *ListRefreshResp) String() string { return proto.CompactTextString(m) } func (*ListRefreshResp) ProtoMessage() {} func (*ListRefreshResp) Descriptor() ([]byte, []int) { - return fileDescriptor_1b40cafcd4234784, []int{20} + return fileDescriptor_1b40cafcd4234784, []int{21} } func (m *ListRefreshResp) XXX_Unmarshal(b []byte) error { @@ -1030,7 +1094,7 @@ func (m *RevokeRefreshReq) Reset() { *m = RevokeRefreshReq{} } func (m *RevokeRefreshReq) String() string { return proto.CompactTextString(m) } func (*RevokeRefreshReq) ProtoMessage() {} func (*RevokeRefreshReq) Descriptor() ([]byte, []int) { - return fileDescriptor_1b40cafcd4234784, []int{21} + return fileDescriptor_1b40cafcd4234784, []int{22} } func (m *RevokeRefreshReq) XXX_Unmarshal(b []byte) error { @@ -1078,7 +1142,7 @@ func (m *RevokeRefreshResp) Reset() { *m = RevokeRefreshResp{} } func (m *RevokeRefreshResp) String() string { return proto.CompactTextString(m) } func (*RevokeRefreshResp) ProtoMessage() {} func (*RevokeRefreshResp) Descriptor() ([]byte, []int) { - return fileDescriptor_1b40cafcd4234784, []int{22} + return fileDescriptor_1b40cafcd4234784, []int{23} } func (m *RevokeRefreshResp) XXX_Unmarshal(b []byte) error { @@ -1118,7 +1182,7 @@ func (m *VerifyPasswordReq) Reset() { *m = VerifyPasswordReq{} } func (m *VerifyPasswordReq) String() string { return proto.CompactTextString(m) } func (*VerifyPasswordReq) ProtoMessage() {} func (*VerifyPasswordReq) Descriptor() ([]byte, []int) { - return fileDescriptor_1b40cafcd4234784, []int{23} + return fileDescriptor_1b40cafcd4234784, []int{24} } func (m *VerifyPasswordReq) XXX_Unmarshal(b []byte) error { @@ -1165,7 +1229,7 @@ func (m *VerifyPasswordResp) Reset() { *m = VerifyPasswordResp{} } func (m *VerifyPasswordResp) String() string { return proto.CompactTextString(m) } func (*VerifyPasswordResp) ProtoMessage() {} func (*VerifyPasswordResp) Descriptor() ([]byte, []int) { - return fileDescriptor_1b40cafcd4234784, []int{24} + return fileDescriptor_1b40cafcd4234784, []int{25} } func (m *VerifyPasswordResp) XXX_Unmarshal(b []byte) error { @@ -1202,6 +1266,7 @@ func (m *VerifyPasswordResp) GetNotFound() bool { func init() { proto.RegisterType((*Client)(nil), "api.Client") + proto.RegisterType((*SamlInitiatedConfig)(nil), "api.SamlInitiatedConfig") proto.RegisterType((*CreateClientReq)(nil), "api.CreateClientReq") proto.RegisterType((*CreateClientResp)(nil), "api.CreateClientResp") proto.RegisterType((*DeleteClientReq)(nil), "api.DeleteClientReq") @@ -1231,64 +1296,68 @@ func init() { func init() { proto.RegisterFile("api/api.proto", fileDescriptor_1b40cafcd4234784) } var fileDescriptor_1b40cafcd4234784 = []byte{ - // 905 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x56, 0xeb, 0x6e, 0xdb, 0x36, - 0x14, 0xb6, 0xad, 0xd8, 0x96, 0x8f, 0xef, 0x9c, 0x9b, 0xba, 0x2e, 0x06, 0xa4, 0x2c, 0x06, 0xa4, - 0x18, 0xe0, 0xac, 0x1d, 0xb0, 0x01, 0x2b, 0xd6, 0x5d, 0xd2, 0x6e, 0x2d, 0xb0, 0x0d, 0x85, 0x30, - 0xe7, 0xe7, 0x04, 0xc5, 0x3a, 0x4e, 0x88, 0x28, 0x92, 0x46, 0xd2, 0x71, 0xb2, 0x47, 0xd9, 0xdb, - 0xec, 0xd7, 0x5e, 0xab, 0x20, 0x45, 0x29, 0xba, 0x38, 0x71, 0xfe, 0xf9, 0x7c, 0xe2, 0xb9, 0x7d, - 0x87, 0xe7, 0xa3, 0xa1, 0xef, 0xc5, 0xec, 0xc8, 0x8b, 0xd9, 0x3c, 0xe6, 0x91, 0x8c, 0x88, 0xe5, - 0xc5, 0x8c, 0xfe, 0x57, 0x87, 0xd6, 0x71, 0xc0, 0x30, 0x94, 0x64, 0x00, 0x0d, 0xe6, 0x4f, 0xeb, - 0x07, 0xf5, 0xc3, 0x8e, 0xd3, 0x60, 0x3e, 0xd9, 0x87, 0x96, 0xc0, 0x25, 0x47, 0x39, 0x6d, 0x68, - 0xcc, 0x58, 0xe4, 0x39, 0xf4, 0x39, 0xfa, 0x8c, 0xe3, 0x52, 0xba, 0x6b, 0xce, 0xc4, 0xd4, 0x3a, - 0xb0, 0x0e, 0x3b, 0x4e, 0x2f, 0x05, 0x17, 0x9c, 0x09, 0x75, 0x48, 0xf2, 0xb5, 0x90, 0xe8, 0xbb, - 0x31, 0x22, 0x17, 0xd3, 0xbd, 0xe4, 0x90, 0x01, 0x3f, 0x2a, 0x4c, 0x65, 0x88, 0xd7, 0xa7, 0x01, - 0x5b, 0x4e, 0x9b, 0x07, 0xf5, 0x43, 0xdb, 0x31, 0x16, 0x21, 0xb0, 0x17, 0x7a, 0x97, 0x38, 0x6d, - 0xe9, 0xbc, 0xfa, 0x37, 0x79, 0x02, 0x76, 0x10, 0x9d, 0x45, 0xee, 0x9a, 0x07, 0xd3, 0xb6, 0xc6, - 0xdb, 0xca, 0x5e, 0xf0, 0x80, 0x7e, 0x03, 0xc3, 0x63, 0x8e, 0x9e, 0xc4, 0xa4, 0x11, 0x07, 0xff, - 0x26, 0xcf, 0xa1, 0xb5, 0xd4, 0x86, 0xee, 0xa7, 0xfb, 0xaa, 0x3b, 0x57, 0x7d, 0x9b, 0xef, 0xe6, - 0x13, 0xfd, 0x0b, 0x46, 0x45, 0x3f, 0x11, 0x93, 0x2f, 0x60, 0xe0, 0x05, 0x1c, 0x3d, 0xff, 0xc6, - 0xc5, 0x6b, 0x26, 0xa4, 0xd0, 0x01, 0x6c, 0xa7, 0x6f, 0xd0, 0x77, 0x1a, 0xcc, 0xc5, 0x6f, 0xdc, - 0x1d, 0xff, 0x19, 0x0c, 0xdf, 0x62, 0x80, 0xf9, 0xba, 0x4a, 0x1c, 0xd3, 0x23, 0x18, 0x15, 0x8f, - 0x88, 0x98, 0x3c, 0x85, 0x4e, 0x18, 0x49, 0x77, 0x15, 0xad, 0x43, 0xdf, 0x64, 0xb7, 0xc3, 0x48, - 0xfe, 0xa2, 0x6c, 0xfa, 0x6f, 0x1d, 0x86, 0x8b, 0xd8, 0xf7, 0xee, 0x09, 0x5a, 0x1d, 0x50, 0xe3, - 0x21, 0x03, 0xb2, 0xb6, 0x0c, 0x28, 0x1d, 0xc4, 0xde, 0x1d, 0x83, 0x68, 0x16, 0x07, 0x71, 0x04, - 0xa3, 0x62, 0x6d, 0xbb, 0xba, 0x61, 0x60, 0x7f, 0xf4, 0x84, 0xd8, 0x44, 0xdc, 0x27, 0x13, 0x68, - 0xe2, 0xa5, 0xc7, 0x02, 0xd3, 0x48, 0x62, 0xa8, 0x0a, 0xce, 0x3d, 0x71, 0xae, 0x69, 0xee, 0x39, - 0xfa, 0x37, 0x99, 0x81, 0xbd, 0x16, 0xc8, 0x75, 0x65, 0x96, 0x3e, 0x9c, 0xd9, 0xe4, 0x31, 0xb4, - 0xd5, 0x6f, 0x97, 0xf9, 0xa6, 0xe8, 0x96, 0x32, 0x3f, 0xf8, 0xf4, 0x0d, 0x8c, 0x93, 0x61, 0xa7, - 0x09, 0x15, 0x73, 0x2f, 0xc0, 0x8e, 0x8d, 0x69, 0x2e, 0x4a, 0x5f, 0x0f, 0x32, 0x3b, 0x93, 0x7d, - 0xa6, 0xaf, 0x81, 0x94, 0xfd, 0x1f, 0x7c, 0x5d, 0xe8, 0x19, 0x8c, 0x13, 0x62, 0xf2, 0xc9, 0xb7, - 0x37, 0xfc, 0x04, 0xec, 0x10, 0x37, 0x6e, 0xae, 0xe9, 0x76, 0x88, 0x9b, 0xf7, 0xaa, 0xef, 0x67, - 0xd0, 0x53, 0x9f, 0x4a, 0xbd, 0x77, 0x43, 0xdc, 0x2c, 0x0c, 0x44, 0x5f, 0x02, 0x29, 0x27, 0xda, - 0x35, 0x83, 0x17, 0x30, 0x4e, 0xae, 0xe0, 0xce, 0xda, 0x54, 0xf4, 0xf2, 0xd1, 0x5d, 0xd1, 0xc7, - 0x30, 0xfc, 0x8d, 0x09, 0x99, 0x8b, 0x4d, 0x7f, 0x80, 0x51, 0x11, 0x12, 0x31, 0xf9, 0x12, 0x3a, - 0x29, 0xd3, 0x8a, 0x42, 0xab, 0x3a, 0x89, 0xdb, 0xef, 0xb4, 0x07, 0x70, 0x82, 0x5c, 0xb0, 0x28, - 0x54, 0xe1, 0xbe, 0x85, 0x6e, 0x66, 0x89, 0x38, 0x51, 0x2d, 0x7e, 0x85, 0xdc, 0x94, 0x6e, 0x2c, - 0x32, 0x02, 0xa5, 0x77, 0x9a, 0xd2, 0xa6, 0xa3, 0xa5, 0xef, 0x1f, 0x18, 0x3a, 0xb8, 0xe2, 0x28, - 0xce, 0xff, 0x8c, 0x2e, 0x30, 0x74, 0x70, 0x55, 0xd9, 0xa4, 0xa7, 0xd0, 0x49, 0x76, 0x59, 0xdd, - 0xa7, 0x44, 0x05, 0xed, 0x04, 0xf8, 0xe0, 0x93, 0xcf, 0x01, 0x96, 0xfa, 0x46, 0xf8, 0xae, 0x27, - 0xf5, 0x2a, 0x58, 0x4e, 0xc7, 0x20, 0x3f, 0x49, 0xe5, 0x1b, 0x78, 0x42, 0xaa, 0x71, 0xf9, 0x5a, - 0xc9, 0x2c, 0xc7, 0x56, 0xc0, 0x42, 0xa0, 0x22, 0x7d, 0xa0, 0x38, 0x30, 0xf9, 0x15, 0xe3, 0xb9, - 0x8b, 0x5b, 0x2f, 0x5c, 0xdc, 0x3f, 0x12, 0x06, 0xb3, 0xa3, 0x22, 0x26, 0xaf, 0x61, 0xc0, 0x13, - 0xd3, 0x95, 0xaa, 0xf4, 0x94, 0xb2, 0x89, 0xa6, 0xac, 0xd4, 0x94, 0xd3, 0xe7, 0x39, 0x40, 0xd0, - 0xf7, 0x30, 0x72, 0xf0, 0x2a, 0xba, 0xc0, 0x07, 0x24, 0xbf, 0x97, 0x00, 0xfa, 0x15, 0x8c, 0x4b, - 0x91, 0x76, 0xdd, 0x86, 0x77, 0x30, 0x3e, 0x41, 0xce, 0x56, 0x37, 0xbb, 0xf7, 0x60, 0x96, 0x5b, - 0x4d, 0x93, 0x38, 0xdb, 0xc5, 0xdf, 0x81, 0x94, 0xc3, 0x88, 0x58, 0x79, 0x5c, 0x29, 0x94, 0x61, - 0x96, 0x38, 0xb5, 0x8b, 0x55, 0x35, 0x8a, 0x55, 0xbd, 0xfa, 0xbf, 0x09, 0xd6, 0x5b, 0xbc, 0x26, - 0xdf, 0x43, 0x2f, 0xff, 0x1e, 0x90, 0x84, 0xce, 0xd2, 0xd3, 0x32, 0x7b, 0xb4, 0x05, 0x15, 0x31, - 0xad, 0x29, 0xf7, 0xbc, 0xfa, 0x19, 0xf7, 0x92, 0x58, 0x1b, 0xf7, 0xb2, 0x4c, 0x26, 0xee, 0xf9, - 0xa7, 0xc0, 0xb8, 0x97, 0x1e, 0x10, 0xe3, 0x5e, 0x7e, 0x33, 0x68, 0x8d, 0x1c, 0xc3, 0xa0, 0xa8, - 0x4f, 0x64, 0x3f, 0x57, 0x68, 0x8e, 0xef, 0xd9, 0xe3, 0xad, 0x78, 0x1a, 0xa4, 0x28, 0x1f, 0x26, - 0x48, 0x45, 0xbc, 0x4c, 0x90, 0xaa, 0xd6, 0x24, 0x41, 0x8a, 0x2a, 0x61, 0x82, 0x54, 0x54, 0xc6, - 0x04, 0xa9, 0x4a, 0x0a, 0xad, 0x91, 0x37, 0xd0, 0xcf, 0x8b, 0x84, 0x30, 0x74, 0x94, 0xb4, 0xc4, - 0xd0, 0x51, 0x96, 0x13, 0x5a, 0x23, 0x2f, 0x01, 0x7e, 0x45, 0x69, 0x84, 0x81, 0x0c, 0xf5, 0xb1, - 0x5b, 0xd1, 0x98, 0x8d, 0x8a, 0x80, 0x76, 0xf9, 0x0e, 0xba, 0xb9, 0x45, 0x23, 0x9f, 0x65, 0xa1, - 0x6f, 0x17, 0x65, 0x36, 0xa9, 0x82, 0xda, 0xf7, 0x47, 0xe8, 0x17, 0x56, 0x81, 0x3c, 0x32, 0xab, - 0x58, 0x5c, 0xb4, 0xd9, 0xfe, 0x36, 0x38, 0x65, 0xad, 0x78, 0xa7, 0x0d, 0x6b, 0x95, 0x7d, 0x31, - 0xac, 0x55, 0x17, 0x80, 0xd6, 0x7e, 0x9e, 0x00, 0x59, 0x46, 0x97, 0xf3, 0x65, 0xc4, 0x31, 0x12, - 0x73, 0x1f, 0xaf, 0xd5, 0xd1, 0xd3, 0x96, 0xfe, 0xbf, 0xf7, 0xf5, 0xa7, 0x00, 0x00, 0x00, 0xff, - 0xff, 0x49, 0x46, 0x0e, 0xa3, 0x00, 0x0a, 0x00, 0x00, + // 973 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x56, 0x6d, 0x6f, 0xdb, 0x36, + 0x10, 0x8e, 0xad, 0xd8, 0x91, 0xcf, 0xef, 0x6c, 0x9a, 0xaa, 0x2e, 0x06, 0x24, 0x2c, 0x06, 0xa4, + 0x18, 0x90, 0xac, 0x19, 0xb0, 0x01, 0x2b, 0xd6, 0x6e, 0x4b, 0xbb, 0x35, 0xc0, 0x36, 0x04, 0xda, + 0xdc, 0x8f, 0x13, 0x54, 0xeb, 0x9c, 0x10, 0x95, 0x25, 0x8d, 0xa4, 0x93, 0x74, 0x1f, 0xf7, 0xe7, + 0xf6, 0x23, 0xf6, 0x67, 0x06, 0x52, 0x94, 0x43, 0x49, 0x6e, 0x9d, 0x7e, 0xf3, 0x3d, 0xbc, 0x17, + 0xde, 0xa3, 0xbb, 0x87, 0x86, 0x7e, 0x98, 0xb1, 0xe3, 0x30, 0x63, 0x47, 0x19, 0x4f, 0x65, 0x4a, + 0x9c, 0x30, 0x63, 0xf4, 0x9f, 0x26, 0xb4, 0x4f, 0x63, 0x86, 0x89, 0x24, 0x03, 0x68, 0xb2, 0xc8, + 0x6b, 0xec, 0x37, 0x0e, 0x3b, 0x7e, 0x93, 0x45, 0x64, 0x0f, 0xda, 0x02, 0x67, 0x1c, 0xa5, 0xd7, + 0xd4, 0x98, 0xb1, 0xc8, 0x63, 0xe8, 0x73, 0x8c, 0x18, 0xc7, 0x99, 0x0c, 0x96, 0x9c, 0x09, 0xcf, + 0xd9, 0x77, 0x0e, 0x3b, 0x7e, 0xaf, 0x00, 0xa7, 0x9c, 0x09, 0xe5, 0x24, 0xf9, 0x52, 0x48, 0x8c, + 0x82, 0x0c, 0x91, 0x0b, 0x6f, 0x3b, 0x77, 0x32, 0xe0, 0xb9, 0xc2, 0x54, 0x85, 0x6c, 0xf9, 0x36, + 0x66, 0x33, 0xaf, 0xb5, 0xdf, 0x38, 0x74, 0x7d, 0x63, 0x11, 0x02, 0xdb, 0x49, 0xb8, 0x40, 0xaf, + 0xad, 0xeb, 0xea, 0xdf, 0xe4, 0x21, 0xb8, 0x71, 0x7a, 0x91, 0x06, 0x4b, 0x1e, 0x7b, 0x3b, 0x1a, + 0xdf, 0x51, 0xf6, 0x94, 0xc7, 0xe4, 0x05, 0x0c, 0x44, 0xb8, 0x88, 0x03, 0x96, 0x30, 0xc9, 0x42, + 0x89, 0x91, 0xe7, 0xee, 0x37, 0x0e, 0xbb, 0x27, 0xde, 0x91, 0x6a, 0xf6, 0xf7, 0x70, 0x11, 0x9f, + 0x15, 0x27, 0xa7, 0x69, 0x32, 0x67, 0x17, 0x7e, 0x5f, 0xd8, 0x20, 0x3d, 0x87, 0x7b, 0x6b, 0xbc, + 0xc8, 0x01, 0xf4, 0xec, 0x46, 0x0d, 0x35, 0x5d, 0xab, 0x4f, 0xcd, 0xd1, 0x2c, 0xcd, 0x50, 0x78, + 0x4d, 0xdd, 0x9f, 0xb1, 0xe8, 0xd7, 0x30, 0x3c, 0xe5, 0x18, 0x4a, 0xcc, 0xb9, 0xf5, 0xf1, 0x2f, + 0xf2, 0x18, 0xda, 0x33, 0x6d, 0xe8, 0x3c, 0xdd, 0x93, 0xae, 0xbe, 0x9d, 0x39, 0x37, 0x47, 0xf4, + 0x4f, 0x18, 0x95, 0xe3, 0x44, 0x46, 0x3e, 0x87, 0x41, 0x18, 0x73, 0x0c, 0xa3, 0xf7, 0x01, 0xde, + 0x30, 0x21, 0x85, 0x4e, 0xe0, 0xfa, 0x7d, 0x83, 0xbe, 0xd2, 0xa0, 0x95, 0xbf, 0xf9, 0xe1, 0xfc, + 0x07, 0x30, 0x7c, 0x89, 0x31, 0xda, 0xf7, 0xaa, 0x7c, 0x76, 0x7a, 0x0c, 0xa3, 0xb2, 0x8b, 0xc8, + 0xc8, 0x23, 0xe8, 0x24, 0xa9, 0x0c, 0xe6, 0xe9, 0x32, 0x89, 0x4c, 0x75, 0x37, 0x49, 0xe5, 0x4f, + 0xca, 0xa6, 0xff, 0x35, 0x60, 0x38, 0xcd, 0xa2, 0xf0, 0x23, 0x49, 0xeb, 0x33, 0xd3, 0xbc, 0xcb, + 0xcc, 0x38, 0x6b, 0x66, 0xa6, 0x98, 0x8d, 0xed, 0x0f, 0xcc, 0x46, 0x6b, 0xd3, 0x6c, 0xb4, 0x3f, + 0x6d, 0x36, 0x8e, 0x61, 0x54, 0x6e, 0x6e, 0x13, 0x1d, 0x0c, 0xdc, 0xf3, 0x50, 0x88, 0xeb, 0x94, + 0x47, 0x64, 0x17, 0x5a, 0xb8, 0x08, 0x59, 0x6c, 0x98, 0xc8, 0x0d, 0xd5, 0xc2, 0x65, 0x28, 0x2e, + 0xf5, 0x77, 0xea, 0xf9, 0xfa, 0x37, 0x99, 0x80, 0xbb, 0x14, 0xc8, 0x75, 0x6b, 0x8e, 0x76, 0x5e, + 0xd9, 0xe4, 0x01, 0xec, 0xa8, 0xdf, 0x01, 0x8b, 0x4c, 0xd7, 0x6d, 0x65, 0x9e, 0x45, 0xf4, 0x39, + 0x8c, 0xf3, 0x69, 0x29, 0x0a, 0x2a, 0xea, 0x9f, 0x80, 0x9b, 0x19, 0xd3, 0x4c, 0x5a, 0x5f, 0xf7, + 0xba, 0xf2, 0x59, 0x1d, 0xd3, 0x67, 0x40, 0xaa, 0xf1, 0x77, 0x9e, 0x37, 0x7a, 0x01, 0xe3, 0x9c, + 0x18, 0xbb, 0xf8, 0xfa, 0x86, 0x1f, 0x82, 0x9b, 0xe0, 0x75, 0x60, 0x35, 0xbd, 0x93, 0xe0, 0xf5, + 0x6b, 0xd5, 0xf7, 0x01, 0xf4, 0xd4, 0x51, 0xa5, 0xf7, 0x6e, 0x82, 0xd7, 0x53, 0x03, 0xd1, 0xa7, + 0x40, 0xaa, 0x85, 0x36, 0x7d, 0x83, 0x27, 0x30, 0xce, 0x67, 0x78, 0xe3, 0xdd, 0x54, 0xf6, 0xaa, + 0xeb, 0xa6, 0xec, 0x63, 0x18, 0xfe, 0xc2, 0x84, 0xb4, 0x72, 0xd3, 0x17, 0x30, 0x2a, 0x43, 0x22, + 0x23, 0x5f, 0x40, 0xa7, 0x60, 0x5a, 0x51, 0xe8, 0xd4, 0xbf, 0xc4, 0xed, 0x39, 0xed, 0x01, 0xbc, + 0x41, 0x2e, 0x58, 0x9a, 0xa8, 0x74, 0xdf, 0x40, 0x77, 0x65, 0x89, 0x2c, 0x57, 0x62, 0x7e, 0x85, + 0xdc, 0x5c, 0xdd, 0x58, 0x64, 0x04, 0x4a, 0xc3, 0x35, 0xa5, 0x2d, 0x5f, 0xcb, 0xf9, 0xdf, 0x30, + 0xf4, 0x71, 0xce, 0x51, 0x5c, 0xfe, 0x91, 0xbe, 0xc3, 0xc4, 0xc7, 0x79, 0x6d, 0x15, 0x1f, 0x41, + 0x27, 0x17, 0x03, 0x35, 0x4f, 0xb9, 0xb2, 0xbb, 0x39, 0x70, 0x16, 0x91, 0xcf, 0x00, 0x66, 0x7a, + 0x22, 0xa2, 0x20, 0x94, 0x7a, 0x97, 0x1c, 0xbf, 0x63, 0x90, 0x1f, 0xa4, 0x8a, 0x8d, 0x43, 0x21, + 0xd5, 0xe7, 0xca, 0x17, 0xc9, 0xf1, 0x5d, 0x05, 0x4c, 0x05, 0x2a, 0xd2, 0x07, 0x8a, 0x03, 0x53, + 0x5f, 0x31, 0x6e, 0x0d, 0x6e, 0xa3, 0x34, 0xb8, 0xbf, 0xe5, 0x0c, 0xae, 0x5c, 0x45, 0x46, 0x9e, + 0xc1, 0x80, 0xe7, 0x66, 0x20, 0xd5, 0xd5, 0x0b, 0xca, 0x76, 0x35, 0x65, 0x95, 0xa6, 0xfc, 0x3e, + 0xb7, 0x00, 0x41, 0x5f, 0xc3, 0xc8, 0xc7, 0xab, 0xf4, 0x1d, 0xde, 0xa1, 0xf8, 0x47, 0x09, 0xa0, + 0x5f, 0xc2, 0xb8, 0x92, 0x69, 0xd3, 0x34, 0xbc, 0x82, 0xf1, 0x1b, 0xe4, 0x6c, 0xfe, 0x7e, 0xf3, + 0x1e, 0x4c, 0xac, 0xd5, 0x34, 0x85, 0x57, 0xbb, 0xf8, 0x2b, 0x90, 0x6a, 0x1a, 0x91, 0xa9, 0x88, + 0x2b, 0x85, 0x32, 0x5c, 0x15, 0x2e, 0xec, 0xf2, 0xad, 0x9a, 0xe5, 0x5b, 0x9d, 0xfc, 0xdb, 0x02, + 0xe7, 0x25, 0xde, 0x90, 0xef, 0xa0, 0x67, 0x3f, 0x28, 0x24, 0xa7, 0xb3, 0xf2, 0x36, 0x4d, 0xee, + 0xaf, 0x41, 0x45, 0x46, 0xb7, 0x54, 0xb8, 0xad, 0x7e, 0x26, 0xbc, 0xa2, 0xf6, 0x26, 0xbc, 0x2a, + 0x93, 0x79, 0xb8, 0xfd, 0x96, 0x98, 0xf0, 0xca, 0x0b, 0x64, 0xc2, 0xab, 0x8f, 0x0e, 0xdd, 0x22, + 0xa7, 0x30, 0x28, 0xeb, 0x13, 0xd9, 0xb3, 0x2e, 0x6a, 0xf1, 0x3d, 0x79, 0xb0, 0x16, 0x2f, 0x92, + 0x94, 0xe5, 0xc3, 0x24, 0xa9, 0x89, 0x97, 0x49, 0x52, 0xd7, 0x9a, 0x3c, 0x49, 0x59, 0x25, 0x4c, + 0x92, 0x9a, 0xca, 0x98, 0x24, 0x75, 0x49, 0xa1, 0x5b, 0xe4, 0x39, 0xf4, 0x6d, 0x91, 0x10, 0x86, + 0x8e, 0x8a, 0x96, 0x18, 0x3a, 0xaa, 0x72, 0x42, 0xb7, 0xc8, 0x53, 0x80, 0x9f, 0x51, 0x1a, 0x61, + 0x20, 0x43, 0xed, 0x76, 0x2b, 0x1a, 0x93, 0x51, 0x19, 0xd0, 0x21, 0xdf, 0x42, 0xd7, 0x5a, 0x34, + 0x72, 0x6f, 0x95, 0xfa, 0x76, 0x51, 0x26, 0xbb, 0x75, 0x50, 0xc7, 0x7e, 0x0f, 0xfd, 0xd2, 0x2a, + 0x90, 0xfb, 0x66, 0x15, 0xcb, 0x8b, 0x36, 0xd9, 0x5b, 0x07, 0x17, 0xac, 0x95, 0x67, 0xda, 0xb0, + 0x56, 0xdb, 0x17, 0xc3, 0x5a, 0x7d, 0x01, 0xe8, 0xd6, 0x8f, 0xbb, 0x40, 0x66, 0xe9, 0xe2, 0x68, + 0x96, 0x72, 0x4c, 0xc5, 0x51, 0x84, 0x37, 0xca, 0xf5, 0x6d, 0x5b, 0xff, 0x87, 0xfd, 0xea, 0xff, + 0x00, 0x00, 0x00, 0xff, 0xff, 0xd9, 0x04, 0xd7, 0x1e, 0xd4, 0x0a, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. diff --git a/api/api.proto b/api/api.proto index 5d9ce1a1e3..cec283d695 100644 --- a/api/api.proto +++ b/api/api.proto @@ -12,6 +12,13 @@ message Client { bool public = 5; string name = 6; string logo_url = 7; + SamlInitiatedConfig saml_initiated = 8; +} + +// SamlInitiatedConfig provides config options for SAML initiated login on a client +message SamlInitiatedConfig { + string redirect_uri = 1; + repeated string scopes =2; } // CreateClientReq is a request to make a client. @@ -22,7 +29,7 @@ message CreateClientReq { // CreateClientResp returns the response from creating a client. message CreateClientResp { bool already_exists = 1; - Client client = 2; + Client client = 2; } // DeleteClientReq is a request to delete a client. @@ -31,7 +38,7 @@ message DeleteClientReq { string id = 1; } -// DeleteClientResp determines if the client is deleted successfully. +// DeleteClientResp determines if the client is deleted successfully. message DeleteClientResp { bool not_found = 1; } @@ -43,6 +50,7 @@ message UpdateClientReq { repeated string trusted_peers = 3; string name = 4; string logo_url = 5; + SamlInitiatedConfig saml_initiated = 6; } // UpdateClientResp returns the reponse form updating a client. @@ -80,7 +88,7 @@ message UpdatePasswordReq { string new_username = 3; } -// UpdatePasswordResp returns the response from modifying an existing password. +// UpdatePasswordResp returns the response from modifying an existing password. message UpdatePasswordResp { bool not_found = 1; } @@ -90,7 +98,7 @@ message DeletePasswordReq { string email = 1; } -// DeletePasswordResp returns the response from deleting a password. +// DeletePasswordResp returns the response from deleting a password. message DeletePasswordResp { bool not_found = 1; } @@ -142,7 +150,7 @@ message RevokeRefreshReq { string client_id = 2; } -// RevokeRefreshResp determines if the refresh token is revoked successfully. +// RevokeRefreshResp determines if the refresh token is revoked successfully. message RevokeRefreshResp { // Set to true is refresh token was not found and token could not be revoked. bool not_found = 1; diff --git a/connector/saml/saml.go b/connector/saml/saml.go index 1c1cb46087..a6ea6fadd6 100644 --- a/connector/saml/saml.go +++ b/connector/saml/saml.go @@ -48,6 +48,10 @@ const ( ) var ( + // DefaultIDPInitiatedScopes specifies scopes to use for IdP initiated flows + // if the target client does not have any configured + DefaultIDPInitiatedScopes = []string{"openid", "profile", "email", "groups"} + nameIDFormats = []string{ nameIDFormatEmailAddress, nameIDFormatUnspecified, @@ -434,6 +438,25 @@ func (p *provider) HandlePOST(s connector.Scopes, samlResponse, inResponseTo str return ident, nil } +// GetSAMLIssuer parses a SAML response and returns its 'Issuer' field. +func GetSAMLIssuer(samlResponse string) (string, error) { + rawResp, err := base64.StdEncoding.DecodeString(samlResponse) + if err != nil { + return "", err + } + + var resp response + if err := xml.Unmarshal(rawResp, &resp); err != nil { + return "", err + } + + if resp.Issuer == nil { + return "", errors.New("response is missing issuer") + } + + return resp.Issuer.Issuer, nil +} + // validateStatus verifies that the response has a good status code or // formats a human readble error based on the bad status. func (p *provider) validateStatus(status *status) error { diff --git a/server/api.go b/server/api.go index 55784b9020..55cbe223a4 100644 --- a/server/api.go +++ b/server/api.go @@ -63,6 +63,10 @@ func (d dexAPI) CreateClient(ctx context.Context, req *api.CreateClientReq) (*ap Name: req.Client.Name, LogoURL: req.Client.LogoUrl, } + if req.Client.SamlInitiated != nil { + c.SAMLInitiated.RedirectURI = req.Client.SamlInitiated.RedirectUri + c.SAMLInitiated.Scopes = req.Client.SamlInitiated.Scopes + } if err := d.s.CreateClient(c); err != nil { if err == storage.ErrAlreadyExists { return &api.CreateClientResp{AlreadyExists: true}, nil @@ -94,6 +98,14 @@ func (d dexAPI) UpdateClient(ctx context.Context, req *api.UpdateClientReq) (*ap if req.LogoUrl != "" { old.LogoURL = req.LogoUrl } + if req.SamlInitiated != nil { + if req.SamlInitiated.RedirectUri != "" { + old.SAMLInitiated.RedirectURI = req.SamlInitiated.RedirectUri + } + if len(req.SamlInitiated.Scopes) > 0 { + old.SAMLInitiated.Scopes = req.SamlInitiated.Scopes + } + } return old, nil }) diff --git a/server/api_test.go b/server/api_test.go index 511d5d39db..df1a499d74 100644 --- a/server/api_test.go +++ b/server/api_test.go @@ -408,6 +408,10 @@ func TestUpdateClient(t *testing.T) { TrustedPeers: []string{"test"}, Name: "test", LogoUrl: "https://logout", + SamlInitiated: &api.SamlInitiatedConfig{ + RedirectUri: "https://redirect", + Scopes: []string{"idtoken"}, + }, }, wantErr: false, want: &api.UpdateClientResp{ @@ -489,6 +493,17 @@ func TestUpdateClient(t *testing.T) { t.Errorf("expected trusted peer: %s", peer) } } + if tc.req.SamlInitiated != nil { + if tc.req.SamlInitiated.RedirectUri != client.SAMLInitiated.RedirectURI { + t.Errorf("expected stored client with SAML initiated redirectURI: %s, found %s", tc.req.SamlInitiated.RedirectUri, client.SAMLInitiated.RedirectURI) + } + for _, scope := range tc.req.SamlInitiated.Scopes { + found := find(scope, client.SAMLInitiated.Scopes) + if !found { + t.Errorf("expected SAML initiated scope: %s", scope) + } + } + } } if tc.cleanup != nil { diff --git a/server/handlers.go b/server/handlers.go index 2b033c870a..1411a79eec 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -19,6 +19,7 @@ import ( jose "gopkg.in/square/go-jose.v2" "github.com/dexidp/dex/connector" + "github.com/dexidp/dex/connector/saml" "github.com/dexidp/dex/server/internal" "github.com/dexidp/dex/storage" ) @@ -395,7 +396,7 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) { } func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request) { - var authID string + var authID, samlInResponseTo string switch r.Method { case http.MethodGet: // OAuth2 callback if authID = r.URL.Query().Get("state"); authID == "" { @@ -403,8 +404,11 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request) return } case http.MethodPost: // SAML POST binding - if authID = r.PostFormValue("RelayState"); authID == "" { - s.renderError(r, w, http.StatusBadRequest, "User session error.") + var code int + var err error + authID, samlInResponseTo, code, err = s.handleSAMLCallback(r) + if err != nil { + s.renderError(r, w, code, fmt.Sprintf("Error processing SAML callback: %s.", err)) return } default: @@ -452,7 +456,7 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request) s.renderError(r, w, http.StatusBadRequest, "Invalid request") return } - identity, err = conn.HandlePOST(parseScopes(authReq.Scopes), r.PostFormValue("SAMLResponse"), authReq.ID) + identity, err = conn.HandlePOST(parseScopes(authReq.Scopes), r.PostFormValue("SAMLResponse"), samlInResponseTo) default: s.renderError(r, w, http.StatusInternalServerError, "Requested resource does not exist.") return @@ -474,6 +478,93 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request) http.Redirect(w, r, redirectURL, http.StatusSeeOther) } +// handleSamlCallback handles a saml callback response with support for the IdP initiated flow. +// if the RelayState is not one of our auth requests, we check to see if it is a valid redirectURI +// for one of our clients. then, if the SAMLResponse is from a registered connector, we create a +// new auth request in the database so we can continue with our regular callback flow. +func (s *Server) handleSAMLCallback(r *http.Request) (string, string, int, error) { + relayState := r.PostFormValue("RelayState") + if relayState == "" { + return "", "", http.StatusBadRequest, errors.New("user session error") + } + // if our relaySate is a valid authId, this is not IdP initiated + if _, err := s.storage.GetAuthRequest(relayState); err == nil { + return relayState, relayState, http.StatusOK, nil + } else if err != storage.ErrNotFound { + s.logger.Errorf("Failed to get auth request: %v", err) + return "", "", http.StatusInternalServerError, errors.New("database error") + } + // if relayState is a valid client_id, we'll check to see if that has config to + // allow the IdP initiated flow. + client, err := s.storage.GetClient(relayState) + if err == storage.ErrNotFound { + // this is not a valid client_id, we have a bogus relayState + s.logger.Warnf("SAML authentication recieved with unknown RelaySate %q", relayState) + return "", "", http.StatusBadRequest, errors.New("user session error") + } else if err != nil { + s.logger.Errorf("Failed to get client: %v", err) + return "", "", http.StatusInternalServerError, errors.New("database error") + } + redirectURI := client.SAMLInitiated.RedirectURI + if redirectURI == "" { + // this client does not have support for IdP initiated login configured + s.logger.Warnf("SAML IdP initiated login attempt made to client %q, but support for this flow is not configured. Update the client config to support this login flow.", relayState) + return "", "", http.StatusBadRequest, errors.New("user session error") + } + // TODO: should we check that 'openid' is one of the scopes? or just put the + // burden on the user to configure these scopes correctly? + scopes := client.SAMLInitiated.Scopes + if len(scopes) == 0 { + scopes = saml.DefaultIDPInitiatedScopes + } + // find the issuer attribute in the SAMLResponse, use it to find the associated connector-id. + ssoIssuer, err := saml.GetSAMLIssuer(r.PostFormValue("SAMLResponse")) + if err != nil { + return "", "", http.StatusBadRequest, errors.New("bad saml response") + } + connectors, err := s.storage.ListConnectors() + if err != nil { + s.logger.Errorf("Failed to list connectors: %v", err) + return "", "", http.StatusInternalServerError, errors.New("database error") + } + var connID string + for _, c := range connectors { + if c.Type != "saml" { + continue + } + var cfg saml.Config + if err := json.Unmarshal(c.Config, &cfg); err != nil { + s.logger.Errorf("Cannot parse saml config for connector %s: %s", c.ID, err) + return "", "", http.StatusPreconditionFailed, errors.New("config error") + } + if cfg.SSOIssuer == ssoIssuer { + connID = c.ID + break + } + } + if connID == "" { + s.logger.Errorf("Cannot find the connector id associated with the issuer %s", ssoIssuer) + return "", "", http.StatusInternalServerError, errors.New("bad saml response") + } + // build our auth request (fake it til ya make it) + authID := storage.NewID() + req := storage.AuthRequest{ + ID: authID, + ClientID: client.ID, + Scopes: scopes, + RedirectURI: redirectURI, + ResponseTypes: []string{"code"}, + ConnectorID: connID, + Expiry: s.now().Add(s.authRequestsValidFor), + } + if err := s.storage.CreateAuthRequest(req); err != nil { + s.logger.Errorf("Could not create SAML IDP initiated request: %v", err) + return "", "", http.StatusInternalServerError, errors.New("database error") + } + // empty InResponseTo value because this is IdP initiated, not in response to any request + return authID, "", http.StatusOK, nil +} + // finalizeLogin associates the user's identity with the current AuthRequest, then returns // the approval page's path. func (s *Server) finalizeLogin(identity connector.Identity, authReq storage.AuthRequest, conn connector.Connector) (string, error) { diff --git a/storage/conformance/conformance.go b/storage/conformance/conformance.go index 8a55a75ba3..1cacc518e3 100644 --- a/storage/conformance/conformance.go +++ b/storage/conformance/conformance.go @@ -246,6 +246,10 @@ func testClientCRUD(t *testing.T, s storage.Storage) { RedirectURIs: []string{"foo://bar.com/", "https://auth.example.com"}, Name: "dex client", LogoURL: "https://goo.gl/JIyzIC", + SAMLInitiated: storage.SAMLInitiatedConfig{ + RedirectURI: "foo://bar.com/", + Scopes: []string{"idtoken", "profile", "email"}, + }, } err := s.DeleteClient(id1) mustBeErrNotFound(t, "client", err) @@ -265,6 +269,10 @@ func testClientCRUD(t *testing.T, s storage.Storage) { RedirectURIs: []string{"foo://bar.com/", "https://auth.example.com"}, Name: "dex client", LogoURL: "https://goo.gl/JIyzIC", + SAMLInitiated: storage.SAMLInitiatedConfig{ + RedirectURI: "foo://bar.com/", + Scopes: []string{"idtoken", "profile", "email"}, + }, } if err := s.CreateClient(c2); err != nil { diff --git a/storage/kubernetes/types.go b/storage/kubernetes/types.go index 5eda178111..d0760481fd 100644 --- a/storage/kubernetes/types.go +++ b/storage/kubernetes/types.go @@ -167,6 +167,14 @@ type Client struct { Name string `json:"name,omitempty"` LogoURL string `json:"logoURL,omitempty"` + + SAMLInitiated SAMLInitiatedConfig `json:"samlInitiated,omitempty"` +} + +// SAMLInitiatedConfig is mirrored from storage.SAMLInitiatedConfig +type SAMLInitiatedConfig struct { + RedirectURI string `json:"redirectURI"` + Scopes []string `json:"scopes,omitempty"` } // ClientList is a list of Clients. @@ -193,6 +201,10 @@ func (cli *client) fromStorageClient(c storage.Client) Client { Public: c.Public, Name: c.Name, LogoURL: c.LogoURL, + SAMLInitiated: SAMLInitiatedConfig{ + RedirectURI: c.SAMLInitiated.RedirectURI, + Scopes: c.SAMLInitiated.Scopes, + }, } } @@ -205,6 +217,10 @@ func toStorageClient(c Client) storage.Client { Public: c.Public, Name: c.Name, LogoURL: c.LogoURL, + SAMLInitiated: storage.SAMLInitiatedConfig{ + RedirectURI: c.SAMLInitiated.RedirectURI, + Scopes: c.SAMLInitiated.Scopes, + }, } } diff --git a/storage/sql/crud.go b/storage/sql/crud.go index e87dc56aa5..a797abd759 100644 --- a/storage/sql/crud.go +++ b/storage/sql/crud.go @@ -476,9 +476,13 @@ func (c *conn) UpdateClient(id string, updater func(old storage.Client) (storage trusted_peers = $3, public = $4, name = $5, - logo_url = $6 - where id = $7; - `, nc.Secret, encoder(nc.RedirectURIs), encoder(nc.TrustedPeers), nc.Public, nc.Name, nc.LogoURL, id, + logo_url = $6, + saml_init_redirect_uri = $7, + saml_init_scopes = $8 + where id = $9; + `, + nc.Secret, encoder(nc.RedirectURIs), encoder(nc.TrustedPeers), nc.Public, nc.Name, + nc.LogoURL, nc.SAMLInitiated.RedirectURI, encoder(nc.SAMLInitiated.Scopes), id, ) if err != nil { return fmt.Errorf("update client: %v", err) @@ -490,12 +494,13 @@ func (c *conn) UpdateClient(id string, updater func(old storage.Client) (storage func (c *conn) CreateClient(cli storage.Client) error { _, err := c.Exec(` insert into client ( - id, secret, redirect_uris, trusted_peers, public, name, logo_url + id, secret, redirect_uris, trusted_peers, public, name, logo_url, + saml_init_redirect_uri, saml_init_scopes ) - values ($1, $2, $3, $4, $5, $6, $7); + values ($1, $2, $3, $4, $5, $6, $7, $8, $9); `, - cli.ID, cli.Secret, encoder(cli.RedirectURIs), encoder(cli.TrustedPeers), - cli.Public, cli.Name, cli.LogoURL, + cli.ID, cli.Secret, encoder(cli.RedirectURIs), encoder(cli.TrustedPeers), cli.Public, + cli.Name, cli.LogoURL, cli.SAMLInitiated.RedirectURI, encoder(cli.SAMLInitiated.Scopes), ) if err != nil { if c.alreadyExistsCheck(err) { @@ -509,7 +514,8 @@ func (c *conn) CreateClient(cli storage.Client) error { func getClient(q querier, id string) (storage.Client, error) { return scanClient(q.QueryRow(` select - id, secret, redirect_uris, trusted_peers, public, name, logo_url + id, secret, redirect_uris, trusted_peers, public, name, logo_url, + saml_init_redirect_uri, saml_init_scopes from client where id = $1; `, id)) } @@ -521,7 +527,8 @@ func (c *conn) GetClient(id string) (storage.Client, error) { func (c *conn) ListClients() ([]storage.Client, error) { rows, err := c.Query(` select - id, secret, redirect_uris, trusted_peers, public, name, logo_url + id, secret, redirect_uris, trusted_peers, public, name, logo_url, + saml_init_redirect_uri, saml_init_scopes from client; `) if err != nil { @@ -543,8 +550,8 @@ func (c *conn) ListClients() ([]storage.Client, error) { func scanClient(s scanner) (cli storage.Client, err error) { err = s.Scan( - &cli.ID, &cli.Secret, decoder(&cli.RedirectURIs), decoder(&cli.TrustedPeers), - &cli.Public, &cli.Name, &cli.LogoURL, + &cli.ID, &cli.Secret, decoder(&cli.RedirectURIs), decoder(&cli.TrustedPeers), &cli.Public, + &cli.Name, &cli.LogoURL, &cli.SAMLInitiated.RedirectURI, decoder(&cli.SAMLInitiated.Scopes), ) if err != nil { if err == sql.ErrNoRows { diff --git a/storage/sql/migrate.go b/storage/sql/migrate.go index dc727535d4..eb8260b2e8 100644 --- a/storage/sql/migrate.go +++ b/storage/sql/migrate.go @@ -229,4 +229,13 @@ var migrations = []migration{ }, flavor: &flavorMySQL, }, + { + stmts: []string{` + alter table client + add column saml_init_redirect_uri text not null default '';`, + ` + alter table client + add column saml_init_scopes bytea; -- JSON array of strings`, + }, + }, } diff --git a/storage/storage.go b/storage/storage.go index 5bbb2b3f1b..26b07db3e4 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -135,6 +135,15 @@ type Client struct { // Name and LogoURL used when displaying this client to the end user. Name string `json:"name" yaml:"name"` LogoURL string `json:"logoURL" yaml:"logoURL"` + + SAMLInitiated SAMLInitiatedConfig `json:"samlInitiated" yaml:"samlInitiated"` +} + +// SAMLInitiatedConfig provides configuration for how a client should be treated when SAML's +// Identity Provider initiated login is used. +type SAMLInitiatedConfig struct { + RedirectURI string `json:"redirectURI" yaml:"redirectURI"` + Scopes []string `json:"scopes" yaml:"scopes"` } // Claims represents the ID Token claims supported by the server.